Day Allocation — Release notes

Site updates, newest first.

← Back to the app

September 2026

v1.8.674

What a review of the form-detachment bug found — including that the mock already knew

The sharpest finding: the answer was already in the repo. tools/mock/ has computed new FormData(#PageForm) on every Save since it was built, and printed only the count. The first Save in the copy after the Name field was moved would have said ProductName had stopped posting — if it had compared instead of counted. It captures the key set as NOVA served it and diffs on every Save now, shouting both what stopped posting and what started. (The warning also passed a second argument to a say() that took one, so the loudest line in the file was silently ordinary. Fixed.)

And one about the previous fix that deserves saying out loud. The sameForm guard added in v1.8.672 was a tautology: it compared a field’s form against a variable that was that field’s form, computed two lines earlier. It could not fail, its commit message claimed “every mover asks this first” when exactly one did, and the test that covered it only checked the call was present. The test now checks the two arguments differ, which is what makes the guard mean anything.

Three real gaps closed. The WEX SKU group was being moved with neither of the two guards its Parent SKU sibling gets. The “this cell is empty now” marker — which the theme hides — was being stamped without checking the cell was actually empty, and a control left in a hidden cell is the worst of both worlds: jQuery Validate skips :hidden, so it stops being validated and goes on posting. And the guard against moving a switch into a hidden cell required both its conditions, when offsetParent is null precisely in the ancestor-hidden case where the cell’s own display still reads normal — so it passed the case it existed for.

The quote page stops painting over the damage. When the inspection column’s heading is blank, the tool no longer falls back to the quoted name. On a swapped line that is the one name known to be wrong — it is the product that didn’t turn up — and a blank heading over a blank stored name is a damaged record, so filling it in from anywhere made the damage invisible. It waits for the fetch, fills from the item’s own inspection when there is a name there, and says NO PRODUCT NAME SAVED in red when there isn’t.

v1.8.673

The Name field goes back where NOVA puts it

“Just move name back.” Done — the move, the click-to-edit lock and their styling are removed outright rather than disabled, because a disabled feature is one somebody turns back on.

Worth keeping the reason in writing: the layout was fine and the mechanism was not. NOVA’s “Inspection for:” heading row is a sibling of #PageForm, so moving the Name form-group into it took a named, posted control out of the form — and saves stopped sending the product name. No amount of care about where a control is drawn is worth a save that silently omits it. If a future round wants the name to read as a title, the way to do it is to style it where NOVA already puts it.

The guard stays, and it is now load-bearing rather than decorative. sameForm() is asked by every mover that touches a posted control before it touches anything, and the test suite asserts each of them asks — not merely that the function exists. The one mover that cannot leave the form by construction (Spares, which refuses any destination that is not its own sibling) has that fact asserted too rather than assumed.

The heading is visible again on its own, rendering as the full product name in bold — which is what that gap held before any of this, and it costs nothing.

v1.8.672

Serious: the Name field had been moved outside the form, and was not being submitted

“Did the name as the title with the click to edit affect saving the name?” Yes. It did, and the question was exactly the right one to ask.

Measured on the live page: NOVA’s “Inspection for:” heading row is a sibling of #PageForm, not a descendant — both are children of the same column. So moving the Name field’s group into that row, which v1.8.663 did, put #ProductName outside the form altogether. Its name attribute is InspectionProductViewModel.ProductName — a real posted field — and a control outside its form is not submitted, and is not validated either.

The Name group now goes to the top of #PageForm instead, which is the row immediately below that heading anyway: it reads the same and it posts. The readonly lock was never the problem — readonly fields submit normally, only disabled ones don’t.

The general fix matters more than the specific one. Every other move this tool makes is cosmetic and reversible; this one silently changed what a save sends, which is the single class of mistake it cannot afford. There is now a sameForm check that every mover asks before touching anything, and a destination in a different form — or in none — is refused outright. It is impossible to repeat by construction rather than by remembering, and the test that would have caught it is in place: the mutation that restores the old behaviour fails two assertions.

v1.8.671

The filled-in name goes in the heading, not beside it

“The names are there but not as the title in the right side.” Measured on the armed page: the name this tool filled in was a <span> in the field cell at 16px/400, sitting immediately next to an <h5> at 20px/500 that NOVA had rendered and left empty. So the name was present and was not the heading — and a row NOVA filled itself looked different from a row this filled, which is the one thing a fill like this must not do.

NOVA leaves the element and empties only the text, so the heading was already sitting there to be used. Writing into it makes a filled row identical to a native one and leaves no styling of ours to keep in sync with theirs. The name splitting now skips a heading whose text this tool supplied — rebuilding its contents would take the marker with it, and that marker is what distinguishes “read off the item’s own inspection” from “the quoted name, because the fetch hasn’t landed yet”, which is the honest part of the feature.

v1.8.670

“No health reading” stops being offered on a battery that has one

“It still asks if I want ‘no x/x’ on a battery I’ve put an x/x on.” The offer is “this battery part has no health reading”, and a figure in the box is that claim’s direct contradiction — but nothing in that control had ever looked at the row’s contents. It checked the Parent SKU, whether the battery was present, whether it was a substitute and whether the fact was already known, and never the one thing staring at it.

Worse than noise, which is why it’s a real fix rather than a tidy-up: pressing it would file “no reading available” against a part he had just read a figure off, and every other body taking that part would then stop being asked for one. A true fact about one cell, recorded as a false one about a whole part.

It skips that row and keeps looking rather than giving up on the item — a kit with two batteries can have a reading on one and not the other, and the offer belongs to the one still missing it. An unfilled X/X placeholder is correctly still treated as no reading: the health pattern wants digits either side of the slash, that case has its own blocking fix, and “no reading available” is a legitimate answer to it.

v1.8.669

Shining to the body, and to the barrel

In Danny’s own words: “grade due to shining to the body or to the barrel depending, it’s because the matte of the body has worn down so it’s shiny.”

Two rows rather than one, because the word he writes changes with the item — a camera body shines, a lens barrel shines — and the customer reads whichever is right for what they sent in. Each carries his wording verbatim and nothing more: the “matte worn down” part is his explanation of why it happens, not what a customer reads, and padding an approved wording with an explanation is how a wording stops being the approved one. Both are lowercase-leading like the rest of the “Grade due to…” family, because they are the tail of that sentence rather than sentences.

The grade is flagged as judgment, and only the grade. The wording is his and is not a guess — but the manual’s Max Grade table has no row for shining at all, checked against the whole transcription. So 9− is this app placing it among its nearest documented neighbours (asset-label residue, cosmetic skin, light peeling to grips — all 9−) and not something the manual says. It renders with a provenance chip for that reason and Ask AI hedges on it accordingly. A test asserts the manual really is silent on it, so if a future reissue adds a row, the flag comes off and its grade is used instead.

v1.8.668

The manual’s sensor ladder and metering faults were missing, and the +kit button was walking left

“The inspection notes drop downs — check the manual for any you might need us to add.” Diffed the manual’s Max Grade table (already transcribed into the process rules) against the fault table the navigator, Grade Wizard and Ask AI all read. Two real holes.

Of the manual’s five sensor-debris rungs, none were in the fault table. The two sensor entries that existed are real cases but neither is on the ladder — so the navigator offered “Sensor” as a part and then showed two off-ladder entries, on one of the commonest and most consequential camera faults there is. All five are in now: f22–f18 at 9−, f22 down to f8 at 8, throughout the range at 7, one or two pieces throughout at 6, and multiple pieces or one clearly visible at any aperture as an outright decline.

Metering had no entry at all — three rows in the manual, two of them declines, reachable from the grade-cap matcher and from nowhere a person would look. It is its own part now: nothing else on a camera behaves like it and it doesn’t belong under Buttons or Dials.

Deliberately, none of the eight carries a customer wording. The manual states these grade caps and gives no approved sentence for them — its one Inspection Notes example is a template with a blank in it — so inventing one would put unapproved copy in front of a customer. The navigator asks the phrase library instead, which has the real sensor wordings.

“The +kit button keeps moving further left after each update to the accessories.” My own bug, from this morning. The stacking offset was a counter kept on the host record — and that record has to survive a render, because it remembers NOVA’s original padding so disarming can restore it, while the buttons themselves are cleared and rebuilt every pass. So each render added the button’s width to a total nothing ever zeroed, and it walked left by its own width every time the table changed. The fix isn’t to reset the counter in the right place, it’s to stop having one: the offset is now the summed width of the inset buttons actually in that cell right now, which can’t drift because it isn’t remembered.

Also: the Spares box is pinned to the same label-left, field-right shape as every other box on the page.

v1.8.667

The lens mark becomes a lens in profile

Picked from a set drawn against the alternatives, then refined twice: “I want a side profile of a lens”, “I like H but I want the front to be rounded”, “the white ellipse needs to be closer to the front”.

The profile answers something the end-on version could not, and it only became visible once all thirteen marks were laid out together: an end-on lens is concentric rings — and so is mount adaptor. At 7mm they were very nearly the same mark. A barrel silhouette can’t be confused with anything else in the set.

Six subpaths filled even-odd: the domed front element, the glass sliver knocked out of it, the barrel, the mount, and two ring gaps. Every gap is a hole rather than a white shape painted on top, so it survives being printed onto something that isn’t white. The dome is a quadratic curve rather than an arc, so its depth is an exact number instead of a sweep-flag guess, and it’s deep enough to still read as curved at 26px — a shallow one flattens back into a straight edge at label size, which is the only size that actually exists here. The glass sits as far forward as it can: the dome’s inner face measures x 2.4–2.7 across the sliver’s height, so at x 3.2–5.0 it is a clean hole in the element rather than a notch out of its edge.

v1.8.666

The inspection column’s missing product name — NOVA’s gap, filled

“Some item names in the right column disappear.” Checked against NOVA’s own HTML before blaming this tool — which it deserved, since it does split names in that column and was the obvious suspect. Fetching quote 71639165 fresh and reading the raw markup settled it: row 0’s heading DD holds “Canon EOS 80D Digital SLR Camera Body”, rows 4 and 5 hold "". Empty at source, before anything runs.

Both blank ones are swapped lines — their notes carry “Not the item quoted” — so NOVA leaves the inspection column with no heading at all and it starts abruptly at “Grade:”. Which is the worst possible line for it to happen on: a swapped line is exactly where “which product is this actually about” is a real question, and it is the one line NOVA declines to answer.

So the tool fills it, and is explicit about which name it used. The name from the item’s own inspection page comes from the fetch that already happens for Spares and accessories, so on a swap this prints what is genuinely on the bench. Before that fetch lands, the quoted name is used and visibly marked as the quoted one — never passed off as the other. It only ever writes into a slot NOVA left empty, and a <dd> is display markup, so nothing here can reach a POST.

v1.8.665

The name becomes a title you can’t mistype into, the SKUs stack under the serial, and the same stacking trap turns up twice more

“Parent SKU and WEX SKU can now go under the serial number now?” Yes — and “now” is the operative word: it only became possible when the Name left row A last release. Measured armed before touching it: the photo band 322px, the left column 459px holding the serial alone, and the right column 185px holding both SKUs. One field in 459, two in 185. That 185 is also the entire reason those two labels needed tooltips — their captions had nowhere to go and wrapped the label instead. All three stack in one column now, the emptied column is marked and collapses, and the width goes back.

“Maybe the name should be a title, with a click-to-edit button.” The protection is the real argument here, not the looks. This is the field the manual’s step 1 is about — an invented or inexact description is rejected at pricing and pushed back — and the release before had just made it the biggest, most inviting-looking thing on the page, sitting where a heading normally is and taking a caret on any stray click. It is readonly now until you press edit (never disabled: a disabled input is not submitted, and this one has to be), the label is gone, and the underline only appears once it is unlocked so there is no doubt which state you are in. A rebuild can’t re-lock a field you are typing in, and disarming gives NOVA its own field back.

“The dropdowns for the parts are behind the input fields.” The same trap as the quote page’s Print All, in a second place. Eight of the nineteen form-groups on an inspection page were computing transform: matrix(1,0,0,1,0,0) — an identity transform, which paints identically to none and creates a stacking context just the same, because the entrance animation fills its last keyframe forever. Anything absolutely positioned inside one is trapped there whatever z-index it asks for. The to frame no longer mentions transform, so it fills only opacity and the transform returns to none when the animation ends; the entrance itself is unchanged.

“The dropdown always opens to the left so you can’t reach it.” Fixed — the panel opens under the chip that was hovered, clamped so a chip near the right edge doesn’t push it off the page. The pointer had been having to leave the chip and cross the whole bar to reach a panel that was only open because the chip was hovered.

“Sensor quality input needs aligning properly too.” The cause was not Sensor Quality. The Grade select was 66px against its 210px, in cells of 249 and 240 — because the Declined label next to Grade was still rendering the visible word at 137px and taking more than half its own cell. Collapsed like the two switches beside it (the word is in its tooltip, the red bin icon is unchanged), the tile is 34px and the select takes the rest.

v1.8.664

Why the quote page’s Print All was never clickable — and two things found with the tool actually armed

“Is the Print All button at the bottom unclickable because it’s in the footer?” Not in it — the footer is empty, 0 descendants, confirmed live. Under it, for a reason no amount of animation tuning could ever have reached.

Measured on quote 71639612: the footer hot zone is a child of <body> at z-index 999998. The button declares z-index 999999 — but its ancestor div.tab-content.top is NOVA’s own, and carries position: relative; z-index: 1. That combination creates a stacking context, and every z-index inside one is scoped to it. So the button’s 999999 is a ranking among its siblings and means nothing at the root, where the whole tab-content block participates at z-index 1 — underneath a footer at 999998. The footer is 55% opaque and clipped to a U whose bottom band is exactly where the tiles peek, so what sits over those buttons is invisible and hit-testable. They paint, they look normal, and every click lands on the footer.

Proved before writing a line of the fix: forcing z-index: auto on that one ancestor moves elementFromPoint over the button’s visible sliver from FOOTER to the button itself, at all three sample points. And it explains why this was only ever the quote page — the inspection page’s Save / Next / Back are direct children of body, no trap, above the footer, always clickable. Same dock, same animation, same hot zone, different ancestry. Every previous attempt to fix this by changing the animation was working on the one page where nothing was wrong.

Two more, found only because the tool was armed on a real page. The “not required” switches were not moving next to their fields on a lens — which is most of the day. The tool itself ticks them on a lens (a lens has no shutter), the theme then hides the input, and the guard that refuses to move a control into a hidden place was testing the field rather than its cell. The cell is not hidden; only the input is, and a switch there is perfectly visible and is the one control that brings the field back.

And the fault navigator was rendering nothing at all, because its data arrives over the data channel and the cached copy was 284 minutes old — so a feature that shipped three releases ago looked like one that had not. That is also the precise cost of removing “Refresh data” two releases ago: redundant right up until a payload field was added, and then the only way to ask. The escape hatch is back, as a single chip that appears only when the fault table is genuinely absent, and asks for it on press.

v1.8.663

The Name field takes the title slot, and the optics ladder gets its last rung

“Can we move the name field to replace the title where it says inspection for?” Yes — and it removes a duplication rather than styling one. NOVA’s heading is “Inspection for: the product name”: the same string as the field, read-only, one row above it. The field itself now takes that slot, full width, at the top of the page, and the heading hides. Full width is the point — this is the field the manual’s step 1 is about (a made-up or inexact description is rejected at pricing and pushed back), and the one that has been truncating since the beginning. In row A it had a column; here it has the page. Unarmed, the heading still renders as the full-name line added a few releases ago, so both states are correct and neither needs the other.

The interaction that would have broken quietly: the photo band was anchored on “the row containing the Name field”, which stops being row A the moment the Name leaves it — the band would have appeared up in the heading row beside a text input. It anchors on Parent SKU now, which is in row A and does not move.

And the optics dust ladder gets its last rung, again in Danny’s own words: “grade 8 heavy dust suggest Grade due to heavy internal dust within the optics.” Worth noting what neither heavy wording carries: the “although this doesn’t affect images in any way” tail that the light and moderate rungs both have. At 9+ and 9− that clause is the point of the sentence; by 8 it would be wrong.

Two mechanism fixes came out of that. The seed guard is a version now rather than a boolean — a boolean guards the seed, so a rung added after it was set never reaches an install that already ran, and answering that with one boolean per rung was already the wrong shape at two. And the dust-vs-debris ranking rule was asymmetric: it demoted a wording that named only debris, which let “dust and debris” through untouched — the commonest shape there is, and the one that sat level with the plain wording wanted on “heavy dust”.

v1.8.662

“Not the item quoted” appears on a real swap, the last button under the Name field goes, and the optics ladder gets its missing rung

“Not the item quoted should appear automatically on item change.” It was not appearing, and the reason is exact: the sentence hung off a change event, and picking a different product from NOVA’s own typeahead sets both the Name and the Parent SKU programmatically. Assigning a value fires no change event — so the one path that genuinely is a swap was the one path this never saw. It only ever fired if you then hand-edited one of the two fields and blurred it. It is polled from the render pass now, with a two-pass settle: the pair has to read the same twice running before the sentence is written, which is what keeps hand-typing a SKU digit by digit from tripping it. That matters because the write is add-only and never removed, so a spurious one would stick.

“Update the app: item changed should be done automatically on save so the button doesn’t need to be there.” Same argument that removed the other four one release ago, and it applies here too: a hand-off earns a button only when it needs a decision, and this one never did. The tool has held the baseline since you armed it, and Save is the moment the change becomes real — so it rides the save, once per arm, wrapped so it can never cost you a save. The row under the Name field now belongs to the versions button, which builds it itself.

And the optics dust ladder gets the rung it was missing — in Danny’s own words, given verbatim: “heavy internal dust and debris within the optics”. Its absence had a visible cost. The previous release taught the ranker to prefer a debris wording over a dust-only one whenever the Dust Level dropdown names debris, and on the optics side there was nothing to prefer: every optics debris phrase in the library said the debris does affect images, and every “does not affect” debris phrase was about the viewfinder. Unlike the three rungs seeded around it, this one is not derived by analogy. A note on the mechanism, because it nearly swallowed this: the seed flag guards the seed, so a rung added after the flag was set would never have reached an install that already had it.

v1.8.661

New: the item, by part — a fault navigator above Inspection Notes

“Maybe above the inspection notes have a navbar populated based on a category — so a camera would have each part, eg sensor, viewfinder, scroll wheel — and hovering over it gives a drop down of vague faults, and hovering over that is a sub menu of faults for that.” Built.

Everything the tool already offers for wording starts from words: the ranked strip needs you to half-know what to type, and the ghost text completes something already begun. This starts from the object. You are holding a camera and the viewfinder is dirty, so you open Viewfinder, see Dust, and pick the severity — no vocabulary needed to get in. Each specific fault shows its grade cap alongside it, so the menu doubles as the grade table for the part in your hand.

The three levels are not invented for this. The Grade Wizard’s fault table has carried them since it was built — its tags are the part, its category is the vague fault, its fault is the specific one — and its types are what make the bar different on a lens from on a camera. A lens offers Optics, Rings and Barrel; a camera offers Sensor, Viewfinder, Rear LCD and Dials; a bag offers the general faults only. Arm before the Category is set and it offers everything, because a bar that is empty on arrival is a bar you learn to ignore. Parts come out front-of-item first rather than in table order. A part with only one vague fault skips the middle level, because a menu whose only job is to be clicked through is a step, not a choice.

The care is in what it types. Clicking a fault writes its approved customer-facing phrase. Twenty-eight of the ninety-two documented faults have no approved wording — they are grade caps — and for those it asks the phrase library rather than writing the fault’s own name, which is the manual’s internal shorthand and not something a customer should ever read. If the library has nothing either, it says so and reports the grade cap instead of inventing a sentence.

And a battery you are replacing is no longer asked for a health reading. “I clicked replace on an LP-E6N and put a Mathorn in, but under it it says no battery health — well obviously I’m replacing it.” Right: Replace means the customer’s cell is not the one going out, so there is nothing to measure — the thing a reading would describe is not there, and the fresh cell that is there is not theirs. The check had been reading “replaced” as another way of saying the battery is present, which is true and beside the point.

v1.8.660

The inset accessory buttons stop landing on top of the field, and on each other

“Fix the buttons in the accessories note input for +kit and see spares, they overlap the right edge of the text input.” Two faults, and his wording names both of them — “+kit and see spares”.

The button was pinned 3px from its wrapper, and the wrapper here is NOVA’s own <td>: a table cell has its own padding and does not hug the input inside it, so the button sat against the cell’s edge rather than the field’s. And every inset button got the same right: 3px — so a row that earns two of them, which Original Box does (it takes “+ kit” and a mark button), put the second exactly on top of the first and then reserved room for the last one measured rather than for the pair.

Now the input’s own box is measured inside the wrapper, buttons stack right-to-left from the field’s actual edge, the running total is what gets reserved as padding, and they centre on the input rather than on the taller cell. The reservation is still set with priority — Stylebot rewrites every declaration in the theme with !important, including input padding, and a plain inline reservation loses to it.

v1.8.659

Declined moves to the Grade, and the versions button stops starting further left than everything else

“Where can we move the decline button that it doesn’t feel out of place?” It felt out of place because the previous release made it so: it shared a row of three with the two “not required” switches, and once those moved to their own fields it was one control alone in a three-column row with two blanks beside it. The same rule that moved them answers it. Declining an item skips Grade, Dust and Sensor Quality in NOVA — so Declined is the switch that retires the grading block exactly as “not required” retires a field, and Grade is the head of that block. Beside it, it reads as “this is why there is no grade” rather than a loose tick at the bottom of a form, and it empties that row completely so the row itself goes instead of standing open around one survivor. It keeps its own red: it is the only one of the three that is a verdict on the item rather than a note about a field.

“The other version/fits button feels too far left.” It was, and by a real amount rather than a feeling. That button had a fallback that inserted it after the field column, which makes it a sibling inside the form-group row — so it started at the row’s left edge, under the label, while everything else in that stack starts at the input’s. Removing the four redundant buttons the release before turned that fallback from rare into normal, and the misalignment came with it. Both renderers now ask for the same row rather than one of them owning it, and the row is built inside the field column, which is what puts it under the input.

And the Spares label is one row again. Its caption was a block in a col-sm-2 label cell, so “Spares” took one line and the caption wrapped to two more — on the one field this theme has spent three rounds trying to lift above the fold. It is a hover tooltip now, and it says considerably more than the three shouted words did.

v1.8.658

The lens mark on a label is filled now

Danny sent a solid camera-lens icon and asked for something closer to it. He is right about this one specifically, and it is worth saying why. A lens seen end-on is concentric rings — so drawn as an outline it came out as rings inside rings, and at 7mm on a label that is four thin circles blurring into a target. Filled, it is a thick ring around a solid disc with a glint knocked out of the glass, and it survives both the size and a cheap label printer. Drawn here rather than traced from anything: ring, gap, glass and glint are four subpaths filled even-odd, so the gap and the glint are real holes rather than white shapes painted on top — which matters on a label that might not be printed onto white.

It is the only filled one, deliberately. Three of the others (converter, mount adaptor, drone) are drawn as bare line segments with no enclosed area at all and would vanish entirely if filled, and the rest read fine as silhouettes. Whether a mark is filled or stroked is now a property of the mark, and the icon and how to paint it come from a single category match rather than two that could disagree.

v1.8.657

The White Box answer was never the Original Box — one wrong list, both symptoms

“Original box keeps being auto ticked and brown box isnt autoticked when added anymore.” Two reports, one cause. The function that decides which accessory row answers NOVA’s White Box popup was matching against the packaging RECOGNITION list — which contains “Original Box” and “Original Packaging” on purpose, because that list answers a different question: does this row count as packaging at all. The comment that introduced the two lists said exactly this, and the code read the wrong one anyway. Original Box is the first row in NOVA’s table, so it matched on position, got ticked as Received, and the function returned before it ever reached the Brown Box added underneath. Original Box is the row that asks the question — it is the thing that is missing — so it can never be the replacement, and it is now excluded twice over. The constant that held the wrong list is deleted rather than left sitting there looking load-bearing.

Four buttons under the Name field are gone. Copy product facts, Tick nova in the app, Tick tested in the app, Refresh data — each opened a tab to say something Danny can say in the app in one click, on a screen he already has open. A hand-off only earns a button when it carries something the app cannot get on its own. “Update the app: item changed” stays for precisely that reason and is the only one that qualifies: what product is actually on the bench exists on this page and nowhere else, it only appears once the line has genuinely changed, and getting it wrong leaves the day’s plan describing an item nobody inspected. Losing Refresh data does not strand anything — the bundle still refreshes itself once it is six hours old.

The tooltips wrap. An absolutely positioned box shrink-to-fits against its containing block, which for these is a label cell about 75px wide — so simply allowing wrapping would have squeezed the sentence into a 75px column. It takes max-content and a max-width together: one asks for the width the text actually wants, the other is what makes it wrap.

And the gap under the navbar now holds the full item name in bold. The answer was already sitting in the hole: what used to fill that row is NOVA’s own “Inspection for: <full product name>” heading, hidden earlier on request. What Danny wanted gone was a shouty page heading; what he wants now is the name readable in full somewhere. Same element, different job — the label shrinks and the bold name carries the line. It replaces a readout that was conditional on the Name field actually clipping, which it stopped doing once row A got wider — so the name stopped being visible in full precisely because the layout got better.

v1.8.656

Six answers from the armed pass: context for the switches, order in the grade list, and what the quote page was not telling him

“Not required” now sits with the field it retires. NOVA puts Shutter Count and Firmware Version in one row and their two retire-switches in the next one, beside Declined — and the theme collapses both labels to nothing, so what Danny actually saw was two identical anonymous tiles under a form, meaning nothing. Each switch is moved into the cell holding its own input, at a size that belongs beside a text field rather than floating on its own, with its words back as a tooltip. Declined stays where NOVA put it: it is not about either field, and it is the one control in that row that should not look like the other two.

The Grade dropdown is in grade order. NOVA serves it as 10, 9-, 8, 7, 6, 9+, 9 — the order the rows were created in, with the two commonest grades on a good body added later and landing below the worst one. On his own caution about it submitting the right thing: the reorder moves the <option> elements, so each label keeps its own value, and the current selection is read before and written back after. Both are tested, and a list already in order is left untouched.

“Elucid uses this” and “BN stock only” are tooltips now. Two goes at those captions and both were wrong: as blocks they added 31px to the document and pushed Spares further below the fold; inline they cost no height but took horizontal room in a label cell about 75px wide, so the label wrapped instead — the same 31px, arrived at sideways. A caption that has to fit inside the layout it is annotating cannot win. On hover they are out of the flow entirely, and can be a full sentence rather than three shouted words.

The gap between the navbar and the first rule was the hole left by a heading. Measured on the live page: the navbar ends at 55px and the rule is at 128. Nothing is meant to be in there — 41px is NOVA’s own clearance for a fixed navbar 25px shorter than the reservation, 16px is the empty row that used to hold “Inspection for: <product>” before that heading was hidden on request, and 16px is the rule’s own margin. The empty row goes with the heading it was holding and the clearance comes down to a real one, which is 36px back at the top of the page that has been fighting to get Spares above the fold.

The quote page says which lines NOVA has an inspection for. The three squares built for the earlier version of this question are the app’s own ticks — Danny’s bookkeeping — and say nothing about whether the work is saved. Read off a real quote before writing any of it: every product block has an inspection column, and NOVA renders the Grade / Cost / Inspection Notes list into it only once an inspection has been saved. Two of that quote’s four lines had it; two had an empty column. So each line now carries SAVED or TO DO, read off the page and shown whether or not the app has ever handed anything over, and the ones still to do get a bar down the side. The app ticks stay beside it, still stamped with their age, because a copy of somebody else’s state is only as true as its timestamp.

Dust and debris are two different faults, and the dropdown already says which. Dust Level lists them separately and the manual grades them on separate ladders, but to a bag of words “debris” was one more token — so a phrase saying only dust could outrank one saying dust and debris on a page where he had just picked the debris option, putting a milder fault than the one he recorded into the sentence the customer reads. The dropdown’s own vocabulary now decides, in both directions. It reorders rather than buries: what he types still wins outright.

Also in this release: the “other versions / fits” button joins the single row of inline actions under the Name field instead of forming a second action area of its own.

v1.8.655

The chip could not mount, and the photo band pushed the form off the page

The first armed look at an inspection page found two things, both mine, both from this morning.

Nothing at all in the navbar. mountBar sat below the stowed early-return, and v1.8.648 made stowed the default — so from that release the chip could never mount and the stub was always the fallback, sitting top-right where it looks like a button that clears the page. Which is precisely what Danny asked to be rid of. It is exactly the shape of the marker bug the same release was tested against: a thing that has to happen whether or not the panel is drawn, sitting after the branch that decides. One test caught that case; the neighbouring one had the same fault and no test.

And the photo band pushed the whole form down instead of sitting beside it. The band takes 33% of row A, and NOVA’s two columns were still 72% and 28% — 133% in a wrapping flex row, so they wrapped onto a second line below the photo. That added about 360px to the page and put Spares back below the fold it had just been lifted above. The columns now share what is left, and only when the band is actually there.

Everything else on that page measured right: Spares moved above the accessory list, the inline action row under the Name field, the clear-unused button above the table, “+ kit” inset into its own field, seven fields carrying their findings, and the panel correctly stowed.

v1.8.654

536 exceptions a page, from one dead line

The first pass over a live page with the tools actually armed found a throw nobody could see. The console of one quote carried 536 identical ReferenceError: tries is not defined in about four minutes — a tries++ inside a 500ms interval, sitting outside its own try/catch so the throw escaped on every single tick. It was the remains of a retry cap whose declaration had gone with the rest of it, and nothing read it.

node --check cannot see that: tries++ is valid syntax and only throws when it runs. So there is now a detector for the shape — any name++ or name += where the file never declares name at all. It is deliberately narrow, which is what makes a failure worth reading; a cleverer scope-aware version would be a linter, and this repo has no build step to hang one off.

And the split names on that page all read CanonEOS7DMarkIIDigitalSLRCameraBody — damage from the build before last, which v1.8.652 then faithfully preserved by stamping the mangled text as the original. That is how a display bug becomes a permanent one. A heading that is long, space-free and full of capitals is now left alone entirely: a real product name has spaces, a reload restores NOVA’s own text, and touching it again only launders the damage.

v1.8.653

The dock goes back to what worked, and Spares comes above the fold

“The Print All button at the bottom stopped working with the CSS change to it being off screen.” That is the answer, and it is better evidence than anything produced remotely — the page reverts injected styles, so every attempt to reproduce it measured the wrong state. Both docks animate bottom again, from a fully hidden -45px rather than the old 20px peek. The transform was for the compositor and it cost the thing the dock is for: the tiles rose and the clicks did not land.

The lag that started all this was never the property anyway — it was thirty-four body:has() rules recomputing the whole document on every mouse move, and that fix stays. So does the fully-hidden rest state, the U-shaped hot zone and its gradient.

Spares now sits above the accessory list. Measured at the real portrait viewport it started at y=1812 with the fold at 1695: off screen on arrival, on the field carrying manual steps 4, 5 and 7 and holding #NOTINCLUDED# — and it could never be on screen at the same time as the accessory list, when steps 5 and 8 are both cross-referencing tasks between exactly those two.

CSS could not fix that: the order is Bootstrap’s column order, a stylesheet cannot reparent, density only got it to the fold, and forcing two columns at 1080 is the change that broke this form once already. So the tool moves the node — above the card rather than below it, so Spares and the top of the list are visible together. It refuses any markup where the two are not siblings, never moves the same node twice, and leaves a view-only page alone.

v1.8.652

The split name was quietly deleting every space

“Ah, the name split is broken.” Two bugs, and the second is the one that mattered.

Each word went into an inline-block with a margin doing the spacing — so the heading looked roughly right and its textContent came back as BlackmagicPYXIS6KforL-Mount, with every space gone. Everything that reads a product name on that page reads it from that heading. On the next arm the tool would have taken the word-salad as the product name, and the lines hand-off, the tested matching and the version lookup would all have been comparing against it. A visual change that silently corrupted the data underneath it.

The space lives inside the word span now, so the text round-trips exactly, and the original is also stamped on the heading so even a split one answers with its real name. The inline-block went too — text already breaks at spaces and never inside a word, so it was solving a problem that did not exist.

And the second bug: it only ever split the first heading. A product block carries one in Products and one in Inspections, and querySelector takes the earlier — so “both sides of the quote columns” was half done. Each heading is now split from its own text, because the inspection side can legitimately name a different product from the one quoted.

v1.8.651

A label that does not print now says so

“The tiles rise but I can’t click them.” That changes the diagnosis: the reveal works, so the click was probably landing all along. The label interceptor calls preventDefault and stopImmediatePropagation first — so anything failing after that leaves NOVA’s own print cancelled with nothing in its place, and pressing the button does nothing at all. Which is indistinguishable from a button that cannot be pressed.

So the printer call is wrapped, a throw is logged rather than swallowed, and a failure now raises a banner in the middle of the screen that stays until dismissed — naming the quote, saying that pop-ups being blocked is the usual cause, and pointing at Shift-click for NOVA’s own label. It used to hang a note off the top of the document, which on a long quote page is above the fold and easy to miss entirely.

A finding that undermines some earlier verification, said plainly: injected styles do not survive on that page. A <style> element added to the head disappears, and even an inline !important on a property the theme does not set is reverted within the same tick. Reading the live page is reliable; changing it to test something is not, and several “verified live” claims this session were of the second kind.

v1.8.650

Print all labels stops depending on the dock

“I still can’t click the Print All button.” Reported twice, and I could not reproduce it. Measured on the same quote at the same portrait viewport: the tile is present, on top, pointer-events: auto, inside the hot zone’s clip, and the reveal rule both matches and outranks the hidden one. Synthetic hover does not drive :hover reliably enough to prove the last step either way.

So this stops arguing with it. The button you press most is now an ordinary button in the page flow, above the products — no hover, no aim, no auto-hiding anything. It drives NOVA’s own Print All control, so the label interceptor sees exactly what it sees on any other press and everything downstream is unchanged.

The dock keeps its copy. The fault there is not understood yet, and removing the other route would be guessing twice.

v1.8.649

Eight things at once: ghost text, the category mark, the app’s ticks on the quote, and the dock you can see

Ghost text in the notes boxes. Type three characters and the rest of the best-matching phrase appears in grey behind the caret; Tab takes it. It never touches the field’s value until you accept — the obvious implementation, inserting the completion as selected text like a browser address bar, would put words you had not agreed to into a box the customer reads, and a save taken mid-suggestion would send them. So it is an overlay, with its metrics copied from the live element rather than assumed, because the theme restyles those boxes and a pixel out reads as a rendering fault. It only completes the current line, and only when the caret is at the end.

Three squares on every quote row — I, N, A — showing what Day Allocation already has ticked, with the age of the answer beside them. The app replies over the tab that any quote-page hand-off already opens, so there is no extra window and no polling. The age is always shown: this is a copy of another origin’s state and it is only as true as its timestamp.

The category mark on the product label, top right. Thirteen drawn silhouettes, one per NOVA Product Category — drawn rather than a webfont, because a font that has not loaded prints a blank box, and this is the one thing the toolchain makes that ends up in someone else’s hands. A category it cannot establish beyond doubt prints nothing at all: for the Print All form the entry’s own SKU has to match the row’s before its category is used.

Shutter count is hours on video kit, and the label now says so. Ticking “not required” there is no longer a footnote but a fix: that box is where the hours go, and the grade cap that depends on them cannot be checked without them. Seen live on a Video Camera with the box retired and the tool quietly checking a figure that could never be entered.

Repair, Missing, Grey Import and Declined are back in the form, with their note boxes directly beneath them. The diagnosis is the useful part: this theme had separated a group NOVA already put together — the toggles pulled out to a bottom dock, the notes floated into a corner — so a tick and the box it reveals ended up at opposite ends of the screen. Putting them back fixes it by construction, and the page got 58px shorter.

A button to clear the unused accessory rows, everything except Original Box and Manual. It never offers a row you have ticked or written on — that is work, and this exists to clear the list before it, not to undo it. It names what it will remove, asks once, and re-reads the list at the moment of deletion rather than trusting what it measured before you confirmed.

Product names split into words on the quote, with the words that differ from what is actually on the inspection picked out — the same treatment the versions picker uses, in the place the difference matters most.

And the dock you can finally see. “I currently can’t click the Print All button” and “maybe add a gradient at the bottom to show you can hover over it” are the same report: once the tiles went all the way off-screen the mechanism was working perfectly and was completely undiscoverable. The gradient is drawn on the hot zone itself, so the clip-path already there does the hard part — the glow appears exactly where the trigger is, tall in the corners, a thin line across the middle. The shape of the hint is the shape of the hit area.

Also: the in-field buttons were overlapping the text, because the room for them was reserved with a plain inline style and Stylebot rewrites the theme’s padding with !important. That is the project’s own rule 5, paid for a third time. Tests 441 → 487.

v1.8.648

The panel stops opening itself, and its buttons move onto the page

“The pre-flight popup in the top right — is that necessary? I don’t ever use it.” It defaults to stowed now: the navbar chip carries the count, and the chip still opens the panel if it is ever wanted. Nothing is lost, because every finding already outlines the field it is about and writes its sentence there, independently of the panel.

That last part turned out to be a correctness question rather than a nicety, and a test caught it: the stowed early-return sits after the marker painting, so stowing hides the panel and never the markers. Had it been the other way round, defaulting to stowed would have silently removed the only part of the tool that gets used. The first version of that test could not tell the difference — it searched forward from the markers, so an early return placed above them still passed. A test that can only see the thing it expects cannot see the thing that breaks it.

The panel’s buttons are inline now — Copy product facts, Tick nova, Tick tested, Update the app, Refresh data, all under the Name field with the versions button and the photo band. They are not left in the panel as well: two places to press for one thing is two places to keep working.

“The Clear above the name isn’t needed, it’s in the nav.” Correct — that was the stowed marker saying “Clear” forty pixels under a chip already reading “Pre-flight All clear”. It no longer draws when the chip is up.

“The tested icon doesn’t stay ticked.” It does now, remembered per quote and keyed by row name rather than index, since an index is only stable within one arm. It says SENT, not done: this side witnesses that the hand-off went, and whether the app matched a line is the app’s to say. A tile claiming “done” for a line the app rejected would be worse than one that reset itself.

And a correction found by measuring on the right monitor: the row A sub-labels were costing 31px of page height as blocks, pushing Spares further below the fold — the exact problem the density work was meant to relieve. Inline they cost nothing.

v1.8.647

“+ kit” and “+ SEE SPARES” move inside the field they write to

“Can we integrate it to the end of the input box instead?” In a table row the note input is narrow, so a button beside it either pushed the row wider or wrapped underneath — and a control about one field ended up looking like a control about the row. Both now sit at the right-hand end of the input itself.

The field is given room for it by measuring the button after inserting it, not by guessing: the label decides the width, and guessing is exactly how typed text ends up running underneath. Both keep a fallback that inserts beside the field if there is no wrapper to position against — a button in the wrong place beats no button.

It also retires a guard without deleting it. “+ kit” used to insert itself, measure the accessories table, and remove itself again if it had pushed the table wider than its container. An absolutely positioned button takes no layout space, so that can no longer happen — but the guard stays on the fallback path, which still inserts beside the field.

The subtle part is teardown. This runs on every render, so recording the “previous” padding each time would mean remembering the value this code set on the render before — and disarming would restore our padding rather than NOVA’s. The original is captured once per element. Nothing the tool does to NOVA’s markup survives disarming, and that has to stay true of the styling too, not just the elements.

v1.8.646

One hero photo, the controls beside it, and everything lined up under the field

“This whole section needs reformatting.” The screenshot showed why. Everything the tool puts under the Name field was being appended to the form-group — which is a Bootstrap row whose two children are the label column and the field column. A third child wraps to a new line and starts at the row’s left edge: under the label, about 120px to the left of the input it is all about. Four blocks, each built at a different time, all inheriting the same misalignment from one helper. Fixed once, in that helper.

“Is it worth having the image as a hero that opens to a carousel, and the other bit to the right of it?” Yes, and it is strictly less: one legible 104px image instead of four illegible 96px ones, the carousel that already existed doing the browsing, the versions button sitting beside it instead of starting a row of its own, and the vertical space back. The scrolling rail and its arrows went with the thumbnails — their whole reason was that six views wrapped to three rows and pushed the form down, which one image cannot do.

The thing that nearly went missing: FRONT and SIDE were marked by two buttons on each thumbnail. Removing the thumbnails would have removed the feature silently. Checked before deleting anything — the viewer had only prev, next and close — so the marking moved in there, which is the better home anyway: judging “this is the front” from a 96px thumbnail is a guess, and the viewer is where the photo is legible. One view can hold one role, so marking a side that is already the front clears the front.

Four mutation checks, including one that removes the marking again and one that drops a type="button" — a bare button inside this form submits it, and on this page that means saving the inspection.

v1.8.645

Grey Import moves to where it is decided

“I never use the popup in the top right, I only use the inline buttons.” That is worth acting on rather than noting: every check whose only output is a panel row would be invisible. The good news is that the tool already works that way — every finding with a field outlines that field and writes its sentence there, and the markers are painted before the panel, so folding or closing it cannot hide them. The panel is an index; the form carries the signal.

Grey Import is now a button beside the serial number — the same principle as the serial controls, the battery row and “+ kit”: the control goes where the thing it is about is. Grey import is decided from the serial and the import markings, so a tick for it three hundred pixels away at the bottom of the screen was a decision made in one place and recorded in another. It shows its current state, and ticked, it says the Inspection Notes still need the 5% deduction wording.

It drives NOVA’s own checkbox rather than shadowing it — a real click, so NOVA’s handlers and the theme’s conditional rules see it exactly as they would a direct one, with a fallback if nothing listened. A second source of truth for something carrying a 5% deduction is not worth having. And NOVA’s own checkbox is untouched, because a page with no control for this at all — on any occasion the tool is not armed — would be worse than one where it is inconveniently placed.

v1.8.644

The phrase ranker uses two things it was throwing away

Where the typed text matches, not just whether. The filter knew only that a phrase contained what you typed and threw the rest away — so a phrase whose wording begins with it scored exactly the same as one that happens to contain those letters two sentences in, and page-context overlap decided between them. Typing is the strongest statement of intent on the page, and it now outranks incidental word overlap by a margin nothing can close — including the category demote, whose own comment already promised you could “type toward” a demoted phrase. This is what makes that true rather than merely possible.

And the mistakes you actually get marked down for. The scored-error list was already on the page, powering a warning when an item resembles a past error. It now also nudges the order of the phrase list. Deliberately a nudge: capped at five repeats and worth about twelve points a token against context scores in the tens, so it breaks ties and reorders near-equals without ever burying what the page actually says. It reads the reason only, never the worked example — the same boundary the warning path had to learn twice, because an example names one battery and would drag every battery phrase up on every item.

Two of the three mutation checks passed first time when they should not have. The “begins with” bonus turned out not to change the order in the test fixture at all — a weaker position rule below it already did that — so the test was rewritten to assert the margin, which is what the bonus is actually sized for. A test that only checks the order of two items cannot see the difference between a rule that decides and a rule that agrees.

v1.8.643

Marking a line tested marks it, with nothing else to press

“Clicking mark tested should mark it in the app, don’t ask.” It used to mark the row and then wait for a second press on a bar at the bottom — a confirmation step that was never asked for. The click sends by itself now.

The batching that needed that second press is a timer instead. It waits 1.5 seconds, and every further click resets it, so ticking four rows in a row is one tab rather than four — every hand-off targets the same named window, so a send per click would navigate the tab the previous payload was still being read out of. The wait is invisible and it is not a confirmation.

What the bar offers during that window is the only thing worth offering: taking it back. The app never unticks, by instruction, so a mis-click that has already gone cannot be undone from NOVA at all. A blocked pop-up keeps the marks and says so rather than going quiet.

v1.8.642

The pre-flight panel gets out of the way by itself

“The pre-flight popup should collapse after 15 seconds if it’s not hovered over.” It sits over the form being filled in, so the useful life of an open panel is however long it takes to read. It folds to the navbar chip now, and everything it does — the checks, the markers, the phrase chips — carries on exactly as before.

The clock is reset by hover and by focus inside it, because using the phrase search means typing with the pointer nowhere near the panel, and “not hovered over” plainly does not mean “abandoned” there. It also re-checks at the moment it fires: if the pointer came back in the meantime, it starts the clock again rather than folding under your hands.

It never folds while a fix is outstanding. Those are the findings that stop a save, and a warning that files itself away after fifteen seconds is a warning you save straight past. Checks and an all-clear both fold; a blocker stays put.

One timer, cleared on every re-arm and on teardown — the same discipline v1.8.634 had to apply to the quote page’s verdict, where a timer per arm each held a stale answer and one painted it at the worst possible moment. The fixture DOM also caught the new code calling matches() and contains() unguarded, which would have thrown mid-render and left a bordered empty box; both are guarded now.

Send Back stays hidden — “I don’t do it, that’s for admin”. It already was, but the new product-row styling nearly resurrected it: an early draft listed it among the controls being normalised, and display: inline-flex on a more specific selector outranks the plain hide. Verified on the live page — as drafted it came back, as shipped it stays gone.

v1.8.641

The product row is one row, and the button you must never press stops shouting

“Edit product, view inspection, print product label and tested seem really disjointed.” Measuring before touching anything found the reason, and it is geometry rather than styling. A product block is 783px wide, so each half-column is about 391px — and the inspection column was being asked to hold Start Inspection (172px), View Inspection (164px) and the Print Label form (198px). That is 534px of buttons in a 391px column, so it wrapped, and the four controls landed on three different lines stepping down and to the right.

Long labels are the cause, so the labels go. They are 34px icon tiles with hover tooltips now — the same language the bottom dock already speaks, which is what makes the page read as one design instead of two. Verified on the live quote: one row instead of three.

And the real find, which is a safety problem rather than a cosmetic one. “Start inspection” — the one control with a standing rule never to touch it, because it changes status in a live system — was the only red button on the row, the loudest thing in it, sitting nine pixels to the left of View Inspection, the one actually wanted. A slip lands on it.

View Inspection is the accent now, and Start Inspection is recessive and exiled to the far end of the row: the gap between them went from 9px to 321px, measured. It stays clickable and it keeps a tooltip that says plainly what it does — it is a real NOVA function, and hiding a control someone may one day need is not a theme’s call to make.

The tested control now carries its state as a class as well as an inline style. Stylebot rewrites every declaration in the theme with !important, so an inline style loses to it whatever its specificity — without a class to hang the marked state on, the theme would have flattened marked and unmarked into the same tile. The inline styles stay as the answer for a browser with no theme installed.

v1.8.640

The quote’s real start time, and a release step that was quietly rewriting history

“Can we set the start time of the quote when it goes into inspection, grab it from the history for a quote if needed.” The app stamps a quote’s start on the first tick, so a quote worked for twenty minutes before anything got ticked reads as twenty minutes shorter than it was — and drift, projected end and the whole trip computer are wrong by that much. NOVA writes the real answer in the quote history, and the tool reads it from there.

Earlier only, never later. NOVA’s timestamp cannot be after the moment you first ticked something, so if what arrives is later than what the app holds, the app’s own stamp is the better answer and is left alone. That is what makes it safe to send repeatedly — and it does send repeatedly, because it rides every hand-off the quote page already makes rather than opening a tab or growing a button of its own. Correcting a start time backwards deliberately does not re-pause anything: the auto-pause is for a quote genuinely starting, and reaching over to pause whatever you are on now is not a correction.

Two guards earned their place. Today only — the quote it was read off has six days of history, and Tuesday’s timestamp dropped onto today’s plan would put the quote hours into deficit the moment it landed. And an item save is not a status change: “Inspection sheet updated for…” appears six times on that one quote, and “Inspection Complete” is the other end of the job entirely. Matching either would hand over a time that means something else.

And the release step had been rewriting the code’s own history. Bumping the version was a plain string replace of the old number across tools/*.src.js — which also rewrote every comment that mentioned it. Those comments are the record of when a decision was made and why, and they get read back constantly. Fourteen notes written across five releases all claimed to be from the newest one, each having been dragged forward by every bump in turn. They have been diffed against their real commits and put back, and the bump is now tools/bump-version.js, which rewrites exactly one line per file and refuses if it finds more than one.

v1.8.639

The tested button could never have synced, and the dock stops stuttering

“The tested button doesn’t sync with the Day Allocation app.” It could not. The send control rendered into a div inside the Quote check card — and that card is folded by default whenever the navbar chip is mounted, which is almost always. render() returns before the card’s body is ever built, so marking a row set a flag, looked for its host, found nothing and returned. Seven rows could be marked with nothing on screen to send them and no way to find out why.

The lesson is more general than the bug: a control that appears because of something on the page must not live inside something that can be folded away. It is now its own element — a small bar that appears at the bottom centre when at least one row is marked, showing the count, a send button and a clear. Bottom centre because the two docks live in the corners and that stretch of the edge is deliberately kept clear.

The dock animation was laggy, and it was not the animation. It was body:has(...) written out once per trigger-and-target pair — nine for the left dock, sixteen for the right, seven for the hot zone and seven more for a stagger. Every one has <body> as its subject, so any hover change near the bottom of the page invalidated and recomputed style for the whole document, thirty-four times over, on every mouse move. :has() takes a selector list and :is() takes one on the other side, so the whole matrix collapses into one rule. The tiles also move on translate3d with will-change, so a compositor layer slides instead of the page repainting behind seven fixed elements, and the opacity fade is gone — a tile ten pixels below the viewport is already invisible. The left-to-right cascade went too: 135ms of deliberate delay on a motion being called slow.

And the trigger is eight times bigger without swallowing anything. A full-width 64px strip would sit on top of whatever the page has scrolled to — View Inspection buttons live down there. So it is a U: two 420px columns at the corners where the docks actually are, joined by a thin 8px bar along the very bottom. clip-path removes the clipped area from hit testing as well as from painting, verified on the live page: 40px up in a corner hits the trigger, 40px up in the middle hits the page underneath, and the bottom few pixels everywhere still catch a mouse slammed at the screen edge.

v1.8.638

The tools arm themselves, and Canon’s checker admits what it cannot answer

“Is there a bookmarklet whose sole job is to run the other bookmarklet on every new page?” There cannot be. A bookmarklet is evaluated in the document it is clicked on and dies with it; nothing it can call outlives a navigation, and that is deliberate on the browser’s part. The thing that does run on every page load is a content script — so the repo’s nova-extension/ plan gained one that loads the tools bundle. It is not installed and Danny has said he does not want to install it, so the bookmark remains the only way the tools arm. The extension is there if that ever changes.

It runs in the page’s own JS world rather than the isolated one, because the versions picker reads NOVA’s own typeahead instance and an isolated script cannot see page JavaScript at all — it would have armed and then failed silently at the one feature that needs it. The bundle is generated from the same sources as the bookmark and is byte-identical to it, and --check now verifies both copies rather than one.

Two things were only correct because a person clicked, and both had to be gated. The automatic capture writes the clipboard; that was argued as acceptable because arming happens at the start of an item, before any copying. Armed on every page load that premise is gone — going back to an item to check something would overwrite a SKU mid-use. And the fresh-data request opens a window, which works only while the arm click still counts as a user gesture; on page load it would be refused, the fifteen-minute back-off stamped anyway, and the live data would simply never arrive again. Neither now fires on an automatic arm. Clicking the bookmark, and the panel’s own Refresh button, still do both.

Set novaAutoArm to off in NOVA’s local storage to turn it off entirely.

Canon’s serial checker only covers products put on the market from 2015 onwards — Danny’s rule, and Canon’s own note. The risk was never the wasted tab: it is that a “not found” answer for an older body reads as a grey-import signal when it only means the checker does not go back that far. The tool now says which it is — and needs no table of release years to do it, because Canon’s own model list is that boundary. A model that matches it is genuinely covered, so “not found” is a real answer; a model with no entry almost certainly predates the cut-off, and a not-found result there means nothing. It used to tell him to hunt the suggestions for an entry that was not there.

v1.8.637

NOVA’s label fields do not mean what they are called

Reading the real print form settled three things and corrected a bug shipped an hour earlier. Quote 71640834’s own form, read live:

productNumber = “1 of 4” — that is the position, not a product number. sku = “12851090” — that is the number actually printed as the big second line. status = “D” — the red flag, handed over directly, and Danny confirms it means declined.

v1.8.636 believed the field names, so every label it produced carried “1 of 4” where the SKU belongs. Fixed. Two more things fall out of it: the position is counted within the quote, so printing one label still says which of the quote’s lines it is rather than “1 of 1”; and the declined line has an empty SKU, which v1.8.636 treated as “no label” and skipped — it would have printed three labels where NOVA prints four.

The entries are now grouped by the index inside the field name rather than by the order the inputs come back in, so one missing field cannot shift every later label onto the wrong product.

A test that crashed instead of failing. While mutation-checking this, a deliberate regression made the harness throw rather than report — and a thrown harness prints no failures at all, so the mutation looked like a pass. The assertions that read a value out of the label are null-safe now: a missing line fails with a message instead of taking the whole run down with it. Worth knowing generally — a test that can crash is a test that can hide the thing it was written to catch.

v1.8.636

The label prints where you are standing, and the barcode is the one NOVA prints

“The print button always opens Day Allocation on the wrong monitor and runs a Google login screen.” It did, and this app was the reason. Printing a label used to hand the job to inspection.dannylaw.dev — a new tab wherever the browser felt like putting it, the whole app booting, and often a sign-in screen in front of a label that needs nothing this app knows. The quote reference is in NOVA’s own URL, the product numbers are in NOVA’s own form, and a barcode is arithmetic. The bookmarklet prints it itself now, in the NOVA tab the click happened in. No hand-off, no second origin, no sign-in, and it lands on the monitor you are looking at.

The barcode was the wrong symbology. v1.8.633 chose Code 39 on a guess, with the note that a scanner set up for NOVA’s output might not read it. Danny’s screenshot settled it: measuring the bars gives 43 elements and 79 modules, in six groups of six each summing to eleven — the signature of Code 128, not Code 39, which would have needed 99 elements. Decoding it gives Start C, the data pairs 71 64 08 34, and a check digit of 52 that matches the one the standard computes. That is 71640834, the quote reference exactly as Danny said — and all three labels on that quote carry a byte-identical barcode, which proves it is not the product number.

Our encoder now reproduces that measured bar pattern character for character. That is the test: not “does it match what we intended” but “is it the same barcode NOVA printed”. The table it uses is the one that has been printing scannable spares labels from this app for months, and five of its rows are independently confirmed against the photograph.

And the layout is measured, not estimated. 75×51mm, Arial Bold, and — the surprise — a single type size for all three lines, not the hierarchy the previous version invented. One of the three real labels has no product number at all, and everything below it sits exactly one line-pitch higher, so the label is built as a top-aligned flow rather than fixed positions. It also reproduces the red flag letter some labels carry at the bottom left, with the position pushed right and the pair centred.

Removed with all this: the Code 39 table and renderer, the label hand-off, and its route through the app. The spares label is a different label printed from this app’s own screen and is untouched. Tests 350 → 344 — fewer, and better: a dozen assertions about a symbology we no longer use, replaced by ones measured off a label that exists.

v1.8.635

Ticking “tested” from NOVA, and the label matching the original exactly

Every line on a quote page now has a TESTED button, and so does the pre-flight panel on an item. It ticks the left box — tested — on that line in the day’s plan. It only ever ticks: unticking stays in the app, where the item is in front of you and what a tick already did (xp awarded, a quote that would stop being finishable) is visible. A remote control over the day’s plan, operated from a page that cannot see it, is not worth the keystroke it saves.

Mark, then send. The quote page collects the lines you marked and sends them in one message rather than one per row. Every hand-off targets the same named tab, so two sends a second apart would have the second navigate the tab the first was still being read out of — a collision the code already warns about elsewhere, and this is the first control designed to be pressed several times in a row. One tab for the whole quote, and nothing to get wrong.

It says what it did: how many were ticked, how many were already done, and how many were not lines on that quote. Sending twice is free. Nothing here guesses at what the app already holds — that lives in another origin behind a six-hour cache, and painting a six-hour-old tick state would be worse than painting none.

The product label now matches NOVA’s exactly. Four things and nothing else, in NOVA’s own order: the quote reference, the product number, the barcode, and “(n of m)” — Arial Bold, 75×51mm. The product name and the spares list, added last release, have come off the face; NOVA’s label carries neither, and spares keep their own label, which is where they were before. The numbers still sit above the bars rather than under them, which is unusual enough to be worth copying rather than fixing: it is how the people handling these have learned to read them.

The tool also stopped sending product names up a URL for a label that no longer prints them.

Still worth ten seconds with a real scanner: the barcode’s value is settled — it is the quote number — but which symbology NOVA’s own generator uses is not, and cannot be read off a screenshot. If a scanner set up for NOVA’s output will not read Code 39, that is a second encoder here rather than a redesign. NOVA’s own Print All is untouched, and shift-click still reaches it.

Tests 319 → 350, the tick logic run for real rather than read: the batch matching is the part that can be quietly wrong, and a source-text test would not have caught the duplicate-line case it now asserts.

v1.8.634

The label print that opened and closed, and a rule that had been broken since it was written

“Something opened then closed when I clicked print.” That was my bug, an hour old. The label hand-off was added to the list of messages that get relayed to whichever app tab is already open — so the tab opened, handed the job over, closed itself, and the tab that received it then tried to open a print window with no click behind it. Browsers block that silently. A label that never prints is the worst outcome this feature has.

The rule was already written in the code, four lines above the list I added to, for the spares label: anything ending in a print window keeps its own tab. Now asserted for both, so the next one cannot repeat it.

And a review found something that had never worked. RULES — the live data the app sends the bookmarklet — was declared a hundred lines below the three constants derived from it. JavaScript hoists the declaration but not the value, so at that point it was undefined and the shutter ceilings, the video hours table and the approved-brand list always fell back to their baked-in copies. The app has been sending all three and they were discarded on arrival. Harmless so far only because the values still match — but adding a third approved battery brand would have been honoured by one check and silently ignored by three others, including the one that caps Grade 10.

That is the project’s own rule 2, shipped again. The existing test models a top-level call that reaches a variable assigned later; it does not model the simpler thing, a module-level constant initialised from one declared below it. It does now, across all ten source files.

Two more, both mine from this afternoon. The quote page’s new verdict started a timer per arm that nothing ever cleared — and each one carried its own findings, so after fixing two downgrades and re-arming, an old timer could paint “2 downgrades unexplained” in the exact place you set Inspection Complete. And the label intercept used stopPropagation, which does not stop a listener on the same element, so one press opened our label and the old spares-label offer — confirming which printed a second, redundant label.

Tests 308 → 319. One of the new ones initially passed while the bug was present, because it matched the comment explaining the method rather than the code using it.

v1.8.633

Our own product label, with the spares on it

Print Product Label now prints ours. It carries everything NOVA’s did — the quote reference, the product number, a barcode of the quote number, and “(n of m)” — at exactly the same 75×51mm, so it goes on the same label stock. A replacement that needs different stock is not a replacement. And it adds the thing that made this worth doing: the spares for that product, which NOVA’s label has no idea exist.

Shift-click still prints NOVA’s own label. This is the only thing the toolchain produces that goes in a box and gets scanned by somebody else, so there has to be a way back that needs no settings screen and works even if everything here is broken. A blocked pop-up says so and points at the same escape hatch, rather than leaving you with no label at all.

The barcode is drawn here, from first principles. No build step and no new CDN, so it is generated as SVG — vector, so it prints at the printer’s resolution rather than a bitmap’s. Code 39 was chosen over the denser Code 128 deliberately: Code 128 needs a modulo-103 checksum, and getting that wrong produces a barcode that looks perfect and is silently unscannable, which on a label in somebody else’s hands is the worst kind of wrong available. Code 39 needs no checksum and is self-checking — every character is nine elements, exactly three wide, exactly one of those a space. The tests assert both invariants across the whole table, and a deliberately corrupted row fails them.

If the quote reference somehow cannot be encoded, it prints nothing and says so, because a label whose barcode encodes the wrong thing is worse than no label. Print-all matches each label to its product by the SKU NOVA sends for it rather than by array position, which NOVA never promised to keep stable.

Worth testing before you rely on it: scan one. If your scanner is configured for Code 128 only it will not read Code 39, and that is a one-encoder change here rather than a redesign. Tests 295 → 308.

v1.8.632

The verdict appears where you set the status to complete

Opening Change Quote Details now shows the quote check right above the status field — “All clear”, or what to look at first. The panel already knew the answer; it was just behind the modal at the exact moment you are deciding, which is the last time anyone looks at that quote.

It distinguishes the three things properly, because they are not the same claim. An unexplained downgrade is a fix and says how many. A soft finding — the note may not cover it — is a check, and is never reported as the harder one. And the roll-up counts: a quote can have nothing unexplained and still not be ready because a line was never saved or was graded by someone else, so those are surfaced rather than being swallowed by an “all clear” that is only true about downgrades. A real finding still outranks all of it.

The modal is a same-origin Fancybox iframe, so its document is reachable. Everything the verdict does is wrapped: a modal that would not open because a message could not be drawn is a far worse trade than no message, and the watcher swallows its own errors accordingly.

The project’s own test caught a real mistake on the way in: the watcher was being started before the roll-up it reports on had been computed, so it would have been watching for a modal with nothing to say. It starts last now.

Tests 269 → 282, including one that fails if a quote with an unsaved line is ever called all clear.

v1.8.631

Which battery a missing health reading is actually about

The “no x/x” button files its answer against a battery part — and it worked that part out from the camera’s SKU, meaning the battery the model ships with. Right for a battery ticked Received; wrong for a substitute, where the cell in the box is a different one. Marking it there recorded a true fact about the cell in your hand against a battery it is not, and every other body taking the genuine part would then stop being asked for a reading.

Your EN-EL15C case gave the fix. “It should be a 15 but they send a c and we can accept it, so I type en-el15c into the box” — so the row’s own comment is where the real cell is written down whenever it differs. When it names a part, that is what gets recorded. When it does not, the button is not offered at all, and the panel says why rather than the control silently vanishing.

Reading the manual for other cases of this shape found the gap in that fix. Its accepted alternatives — Mathorn and Hähnel — are marked Received, not Replace: “select Received and comment only the brand name”. So a guard that only looked at Replace let the commonest substitute of all straight through. It now covers both.

Two more from the same pass. A replaced protective cap with no comment was not flagged at all — the check only fired when a comment existed and was wrong, so the commonest way to get it wrong, leaving the box blank, produced nothing. And the third-party fallback wording still carried the older manual’s “3rd Party (Unbranded)” where the current one says exactly “3rd Party”. That fallback never fires in practice, but a literal contradicting the authority is exactly what cost three changes by two readers in two days.

Checked and already correct: Grade 10 with a third-party battery or charger capping at 9+, third-party wording rejected on batteries and chargers, and the Figure 8 / Kettle lead cross-reference.

Tests 260 → 269. One guards a bug this nearly shipped with: the named part was read from the URL after being deleted from it, so it was always empty — the feature would have done nothing while appearing to work.

v1.8.630

Saving an item ticks nova, and a NOVA decline writes itself

Saving in NOVA now ticks nova on that item here. It rides the hand-off the save already sends, so there is no extra tab and nothing to press. It names the item, not just the quote — a bare quote reference means the whole quote, and ticking five lines because one was saved is a bug this already had once. Re-saving something you went back to edit costs nothing: the answer is “already ticked, no change”.

And a decline made in NOVA is written here, with NOVA’s own reason. You have already made the decision and typed the wording on the other screen; being asked to pick it again was asking the same question twice.

The wording goes in as the note on an “Other” decline rather than as a reason of its own. The app’s reasons are keys from a curated list, and a custom one is stored as its own label — so putting a sentence of free prose there would drop that sentence into the reason dropdown for every future decline. “Other” plus the note is exactly what the app’s own decline modal produces for wording that is not on the list.

Missing is deliberately still asked about. Declining is app-local: three flags and the XP an inspect+nova tick would have earned. Marking something missing queues a −1 against the Wex tracker, and both its wording and whether the quote number gets tagged “(NR)” depend on an answer NOVA does not hold — genuinely missing, or removed from the allocation. And a decline that arrives with no wording at all still gets asked, because the app requires a note on an “Other” decline and writing one without would create a state its own interface refuses to produce.

Tests 248 → 260. One of them guards the v1.8.615 bug against reappearing in this second path: declining something already marked missing and already sent must reverse the tracker −1 rather than orphan it.

v1.8.629

Hand-offs reach the app tab you already have open

“Sometimes it doesn’t open the Day Allocation we already have open on my second screen.” Found it. The relay decided whether that tab existed from a heartbeat under fifteen seconds old — and Chrome throttles setInterval in a tab that is not the active tab of its window, down to roughly once a minute. A perfectly alive app tab on the second screen therefore looked sixty seconds stale, the relay was skipped, and a new tab opened instead of the payload landing where you are looking.

Widening that window on its own would have been worse than the bug. A tab that had genuinely been closed would then look alive too, and the hand-off would be relayed into nothing and lost with no trace at all. So the window widens and the receiving tab now acknowledges: the sender waits for that receipt, closes only when it arrives, and with no receipt keeps the query string and handles the hand-off itself, exactly as a single-tab setup always has.

The failure mode is now “an extra tab opened” rather than “the thing you did never happened”, which is the right way round.

The receipt is written before the receiving tab does any of the work, deliberately. Acknowledging afterwards would let a slow confirm or a Firestore round-trip push it past the sender’s patience, and the same hand-off would then be applied twice — once in each tab. Taking delivery is the fact being reported; what happens next is not the sender’s business. A test asserts that ordering.

Tests 242 → 248, the handshake exercised across two simulated tabs: acknowledged, unacknowledged, and a receipt meant for somebody else.

v1.8.628

Serial numbers against import status — recorded, never asked about

Every saved inspection with a serial now records the serial against whether Grey Import was ticked. The tool has always read both halves on every item and never once kept them together; this is one append away from a dataset, so it appends.

It is the one hand-off that shows nothing. No modal, no banner, no toast, no queue entry — it rides the same single tab-open the capture already uses, so it costs nothing extra, and it lands in the arrivals feed so it is not invisible. This is a measurement, not a decision, and anything louder would be asking you to confirm a data point.

What is stored is the serial's shape alongside it — letters as A, digits as # — because the question is whether the format predicts the answer for a given brand, not which individual serials were grey. Length is kept deliberately: an 8-digit and a 9-digit serial are different formats, and collapsing runs would hide exactly the difference being looked for. Re-saving the same unit corrects its row rather than adding a second, since a doubled row would bias the rate this exists to measure.

A table appears in the Guide once there are twenty units behind it, grouped by brand and then by shape, and it appears for a brand only when more than one shape has been seen — one format tells you nothing about whether the format predicts anything. It makes no claim: it shows counts and says how many they are built on.

On the privacy boundary, written down rather than assumed: quote reference, product, SKU, the serial and one boolean. A serial identifies a unit, not a person, and it goes to your own app on your own device and nowhere else. It is the first per-unit identifier this hand-off has carried, which is why it is stated.

Tests 226 → 242. One mutation checked that the new checker is wired into both router lists — the failure from v1.8.615, where Missing/Declined worked only when no other app tab was open.

v1.8.627

Five things a review found in the versions picker, four of them mine from today

A lens hood was a version of the lens it fits. v1.8.626 taught the model tokens that a product’s identity is what comes before “for”, and left the focal length reading the whole string. Since a focal match overrides everything, “Canon ET-87 Lens Hood for EF 70-200mm” matched the 70-200 itself — and the quote page printed “Same product, different version” for a hood against its own lens. The opposite of the truth, on the one block whose job is saying the item is wrong.

The picker could paint a stale answer over a correct one. Each search has three routes to its callback — the async one and two timers — and nothing said which search a set of results belonged to. Worse than a plain race: the catalogue cancels the previous request when a new search starts, so the older query is guaranteed to be resolved by its own four-second timer with nothing in it. Type to narrow the seed, see the right answer, then watch “Nothing found” replace it three seconds later, with the row indexes stale too.

A half-finished swap looked like a finished one. Firing NOVA’s own event only surfaces a synchronous throw, and NOVA sets the SKU and refetches accessories in an AJAX callback — so a failure there left the new name sitting over the old product’s SKU and accessory rows, under a note saying it had worked. The pick’s SKU is known, so it is now checked afterwards and says so if it did not take. And a refused swap puts the name back, instead of showing the new product beside a note saying the change was refused.

The picker did not work for camera bodies. Wex body names carry two trailing nouns — “Canon EOS R6 V Digital Camera Body” — and the seed stripped one, so the mark survived and the search found only that mark. Exactly the failure a test already guarded for lenses, one product category over, which a single-pass strip could not see.

And a note was landing inside the typeahead wrapper — the v1.8.600 bug class. An input hands its note to its parent, and the Name field’s parent is that wrapper, so the suggestion dropdown would have opened below it. Notes climb out now, for every caller rather than just this one.

Also fixed before the review: esc() escapes & < > and not quotes, and it was being interpolated into a value="…". A product name with an inch mark would have broken the picker. Tests 215 → 226.

v1.8.626

The version rules, checked against 389 real product names instead of six

The matching was wrong and I had not noticed, because every example I tested it on was Sony. Pulled 389 real product names out of NOVA’s own search and ran the shipped rule over every pair: it called a Fujifilm XF 14mm a version of a Canon RF 14mm, an Olympus 45mm f1.2 a version of a Canon RF 45mm f1.2, and a Laowa a version of a Sigma. 400 pairs matched that shared almost no words at all.

Two rules fix nearly all of it, and both came out of that data rather than out of my head. The brand has to match — obvious in hindsight, invisible while every example was one brand. And a product’s identity is what comes before “for”: everything after describes what it fits. Without that, “Canon BG-R10 Battery Grip for EOS R5 / R6” was a version of the EOS R6 body, because the grip names the body it fits. Loose false matches went 400 → 4, and the four that remain are arguably right — two Nikon 500mm f4 AF-S variants the catalogue names differently.

A different mount is kept, deliberately. Mounts are read but never used to reject: “different fits” is half of what this is for, so a Sigma 15mm for Canon RF and the same lens for Sony E belong in the list. 36 such pairs in the sample. They are labelled rather than filtered.

And the names break up. Every row in the picker is by definition almost the same string as every other, so the eye has nothing to catch on — the Sony 16-50 and its Mk II differ by three words buried in fourteen. The differing words are now marked, using the same diff the quote page uses.

Brand detection follows product-matcher’s own detectBrand — that project exists to match product names and had already solved this, including the multi-word list that stops “3 Legged Thing” being read as brand “3”. Tests 203 → 215, every fixture a real name off the live catalogue.

v1.8.625

Product captures stack into a list instead of a modal per save

“Instead of opening a modal every time, just add a list on DAILYALLOC for them to go through later.” Every save sent a capture, and every capture opened the gear router — mid-item, on the other screen, once per item all day.

Worse, and not obvious: they did not accumulate. The park was a single slot, so the second save of the day silently overwrote the first. Anything not dealt with on the spot was simply gone, with nothing to say so.

They stack now. One entry per product, keyed on SKU so re-saving the same item updates rather than duplicates, newest first, capped at forty and aged out after a week — a capture describes a product rather than a moment, so it keeps well, but a week-old one is scenery. Arrival parks it and says how many are waiting; the banner counts them alongside the other hand-offs, and Review works through them one at a time. Filing one removes that one and says how many are left. A single capture parked by an earlier version is migrated rather than dropped — it is somebody’s unfinished work.

Tests 192 → 203. Four mutations were checked and two initially passed: reverting to a single slot, and making the router clear the whole list when one capture was filed. The second only failed once the test asserted the router’s call site rather than calling the helper itself.

v1.8.624

The two names, side by side, with only the difference marked

“Make ‘Not the item quoted’ stand out — maybe a diff between the names.” It used to be a sentence saying the product on the bench is different, which sends you off to compare two names yourself, in two columns, one of which you have to scroll to. The names are the finding, so both are shown together with only what differs marked.

Case-insensitively on purpose: “f3.5-5.6” and “F3.5-5.6” are the same aperture, and marking that would bury the real difference. The diff finds a common head and a common tail, because product names differ by an insertion in the middle — “… OSS Lens” against “… OSS II Power Zoom Lens” — and a head-only diff would mark the trailing “Lens” as different too.

The note underneath now says which problem it is, because a Mk II and a completely different lens are not the same situation and should not read the same. Same focal length reads “same product, different version”; a different one reads “this is not a version of the quoted item”.

The product-identity rules moved into a shared file, since the quote page needs the same ones and cannot reach anything inside the pre-flight’s closure — the tools run one per page. The test harness loads that file the way the real bookmarklet does; without it every version check would have silently taken the no-helper fallback and the suite would have kept passing while testing nothing.

v1.8.623

Versions and fits: a different focal length is a different lens

“There’s some that are completely different focal lengths.” Correct, and it was in my own test output before this shipped: seeding Sony E 16-50mm f3.5-5.6 OSS returned the Mk II and the original — and then the 18-55, the 18-200, the 18-135 and the 70-350. Four of six results were a different lens. I read that list, noted the top two were right, and did not look at the rest.

NOVA’s search is fuzzy and has no idea it is being asked “which versions of this”, so the narrowing has to happen on our side. For a lens the identity is the focal length: 16-50 and 18-55 are not versions of each other however many words they share, and no amount of other similarity overrides that. For a body there is no focal length, so the model token does the same job — R5, A6000, 1000D — with mark numbers and apertures deliberately excluded from counting as models.

Nothing is thrown away. Close matches sort first, everything else follows under a line saying so. The rule is a judgement about what counts as the same lens, and a judgement should not be able to hide the answer — if the one you want is an odd case, it is still on screen.

Tests 170 → 184, and the fixture is the real result list off the live page rather than invented strings. Two of the new tests exist because mutation runs caught them being wrong: one asserted a mark number was excluded using “II”, which carries no digit and never reaches that guard at all, so deleting the guard left the test passing.

v1.8.622

What is hidden but would still save, and how often a Wex photo actually exists

The panel now says when a hidden field would still be saved. The theme hides Shutter Count and Firmware behind their “not required” boxes, and Sensor Specifics unless Sensor Quality reads marks with effect. NOVA does none of that hiding, and none of the clearing that would go with it — verified on a live inspection: a field holding 7000, ticked not-required, was invisible and still sent 7000 in the form’s own POST.

This had nowhere else to live. It cannot be fixed in CSS, because display:none does not stop a field submitting and un-hiding it turns NOVA’s numeric validation back on, which blocks the save — tried and reverted the same day. And it must not be fixed by clearing the field, because that is the tool overruling NOVA rather than matching it. So it is reported: the finding names the field and the exact value that would be written, and the decision stays yours.

Sensor Specifics gets the same treatment, covering the case its original sign-off never asked about: text typed while Sensor Quality read “marks”, then the dropdown changed for any reason. The description survives, invisible, and would ship alongside a clean sensor.

And the panel now counts how often a Wex photo exists at all. The photo tool’s own source has always said the missing-image gap is “worth knowing how big it is before paying for it”, and nobody has ever known. One vote per SKU so re-arming the same item cannot inflate it, misses keep their names because which kinds of thing miss is the real question, and the running rate appears in the panel once there are a dozen items behind it. Local to the browser; nothing is sent anywhere.

That number decides the rest. A free image search (Wikimedia Commons) was tested against a real quote: it found mainstream bodies and lenses, returned nothing at all for the tripod, the radio mic and the raincover, and confidently returned a lens mount adapter for a camcorder. So if the misses are mostly accessories and pro video it is worth nothing, and if they are mostly bodies and lenses it is free and sufficient. Tests 162 → 170.

v1.8.621

Other versions and fits — pick the Mk II without retyping the name

A button beside the Name field opens NOVA’s own product catalogue, filtered to this item’s siblings. The data is NOVA’s: #ProductName carries a typeahead whose dataset is a search over NOVA’s products, it is same-origin, so it can simply be asked — no key, no cost, no third party, and asking does not touch the field.

The search term is the current name with the tokens that make it specific to one version removed — a trailing mark number and the trailing noun. Searching the full name finds that exact product and little else, and the whole point is to find its siblings. The box is editable, because the guess at what the variants share is a guess.

The swap is NOVA’s, not ours. inspection.js binds typeahead:select on that field, and that handler is what sets the Parent SKU, flags the name as changed, and re-fetches the accessory mapping. So a pick sets the value through typeahead’s own API and fires NOVA’s event with the chosen suggestion, exactly as its autocomplete does. Setting the two inputs by hand would have produced a page that looked swapped and still carried the previous product’s accessories.

Verified end to end on a real quote, then reloaded to discard it: an item whose inspection read “Sony E 16-50mm f3.5-5.6 OSS Lens” while the quote said “OSS II Power Zoom” — the exact not-the-item-quoted case. The seed found the Mk II as the top hit; picking it moved the SKU from 1532763 to 3182481, set the changed flag, and NOVA rewrote the accessory list from the old lens’s parts to the Mk II’s. That last one is what hand-setting the fields would have missed.

The button only appears when NOVA’s search is actually on the page, so there is never a control that cannot do anything. Tests 153 → 162, and one of them caught a real ordering bug in the search term: with the noun stripped second, “Sony FE 24-70mm f2.8 GM II Lens” kept its II and would have found only the Mk II — defeating the entire point.

v1.8.620

The photos scroll instead of wrapping, and NOVA turns out to have no CSP at all

One scrolling row, not a wrapping block. Six thumbnails wrapped to two or three rows in the Name field’s column and pushed the whole form down — and on a portrait screen the identity block is where the eye starts, so it should not move because a product happens to have seven views. It is one row that scrolls now, costing a single row of height whatever the count, with ‘ and ’ arrows that appear only when it actually overflows. Offering a control that does nothing is worse than not offering one. Both strips got it — the pre-flight’s and the standalone one.

NOVA sends no Content-Security-Policy header at all. The project notes said the opposite — that it blocks connect-src and form-action — and that claim had shaped at least one design decision. Read off the live headers: no CSP header, no meta tag, and fetches to googleapis.com, google.com and the Wex asset server all succeed from a quote page. The note is corrected, dated, and says to re-read the headers rather than trust it, since Wex can add one whenever they like. What is still true is CORS, which is the actual reason a NOVA page cannot read app.js.

That matters for the “search Google when there is no Wex photo” idea, which is now on the list with its real blocker written down: not the CSP, but the key. A search API key in a bookmarklet is public — it lives in this repo and in the bookmark itself — so it would have to go through the Cloud Function the way the Anthropic key does. The photo tool’s own source already says the gap is “worth knowing how big it is before paying for it”, so the next step is to measure the miss rate, which costs nothing.

v1.8.619

The quote panel stops owning a third of the page, and its x stops being a delete key

The quote panel had never learned what the pre-flight panel took four versions to learn. It was a fixed 360px overlay at top:80 right:24 — the exact geometry measured and rejected for the other panel, 20px wider — on a page whose content runs the full width of a portrait screen. So it covered the right third of every quote row, including the column its own inline blocks get inserted into.

And its only control called remove(). The count, the roll-up and every click-to-jump target went with it, with no way back short of re-arming the bookmarklet. That is exactly the failure fixed for the pre-flight in v1.8.558, where “this box is in the way” was silently answered with “then you get none of it”. It now has a chip in the navbar, starts folded, and the x stows to a dashed stub you can click to bring it back.

The chip placement is now one implementation, not two. It measures NOVA’s navbar for the widest gap between elements that are genuinely painted — leaves, not containers, since a container’s box spans the whole row and reports no gaps at all on a navbar that is two-thirds empty. It then checks where the chip actually landed: off-screen was the only failure the old code knew about, but sitting on top of your account menu is just as useless and looks like a bug rather than a fallback.

The card hanging off it snaps to the page edge when the chip sits inboard. Right-aligning to the anchor is the dropdown convention and the wrong result here — the chip mounts around 463px, so a 340px card landed in the horizontal middle of the form, covering label-to-input relationships rather than the ends of fields.

Tests 142 → 153. The placement ones run against a stub navbar shaped like the real one, and were checked by breaking the code four ways: measuring containers as well as leaves reproduces the original “no gaps” result exactly.

v1.8.618

The palette reaches what it dresses, and the one warning that matters stops whispering

Colour tokens now live on :root, not on the panel. Everything this tool injects goes into NOVA’s own markup — phrase chips, sentence pills, mark and health and kit buttons, the photo strip, the name read-out, the receipts — and none of it is inside a .pf element, so the palette was out of reach and all of it hardcoded the dark branch: 49 literals across six colours. The panel adapted to a light page; the twelve things it scattered over the form did not, at roughly 1.1:1. That is the default state for anyone without your theme. 59 literals now resolve through the tokens, and a test fails if a new one appears.

The stale-bookmark warning was set as the least important text in the panel. It was concatenated into the eyebrow — 11px, faint, uppercase, tracked: the smallest and lowest-contrast style in the stylesheet, and the worst available setting for a sentence. Its own comment said a stale bookmark makes every other thing the panel says unreliable. Worse since the navbar chip landed, because with a chip the eyebrow is the header, so seventy characters of tracked caps wrapped to four lines — and the chip, the only thing visible while folded, never mentioned it. It now gets its own row above the list, and the chip goes red and reads STALE ·.

The panel can clip. .pf declared no overflow, so the inline max-height never clipped anything: with a long list the footer and the wrapped action buttons painted outside the panel’s border and backdrop, over the form with no background behind them — at their least readable exactly when the panel is busiest. It is a flex column that hides overflow now, and the findings list takes the leftover height instead of a fixed 46vh.

The card was folding out over the middle of the form. It right-aligns to the chip, which is correct for a dropdown; but the chip mounts at 463px in the navbar’s empty band, so a 340px card landed at 273–613 — the centre of a form running 16–1049, covering label-to-input relationships rather than the ends of fields. It snaps to the page edge when the chip sits inboard.

The shared photo bookmarklet was dark-only, which is backwards: it is the one tool guaranteed to meet stock NOVA, so a white page is its default case. Its two most important lines — “looking for photos” and “no photo for this SKU” — were rendering at about 2.2:1, so a colleague getting no result would have read a barely-visible grey line and concluded it was broken. It follows the page now. Its caption also carried a literal newline with no white-space set, so the keyboard hint ran onto the caption line; and Close sat between Previous and Next, making the button you hit by accident while paging the one that ends the session. Both fixed and checked on a real page.

Also: the ni note tier carried the longest prose in the tool set in 10px tracked uppercase — the one place that treatment was applied to a sentence rather than a three-word label. And a folded chip and a stowed chip rendered identically while the click did two different things; stowed is dashed now. Tests 131 → 142.

v1.8.617

NOVA’s hand-offs wait their turn, and a photo bookmarklet you can hand to someone else

Nothing from NOVA interrupts you now. Every hand-off that asks a question used to open a confirm the moment it landed — which is the moment you pressed Save, mid-item, on the other screen, with your hands on the next thing. The answer is rarely urgent and the interruption always is.

The four that ask something (Missing/Declined, a changed item, a quote’s line list, and finish-this-quote) are queued and surfaced as a banner at the top of the app, next to the preparer one and shaped the same way. Review works them oldest-first, one at a time. The ones that don’t ask — ticking nova, remembering spares, image marks, a battery with no health readout — still apply straight away: queueing those would mean asking you to confirm things nobody was asking about. A parked gear capture is counted in the same banner so there is one place to look.

When there is only one thing waiting the banner names it, because “1 thing from NOVA” makes you open it to find out what. The queue ages out after sixteen hours — a day’s plan moves on, and a queue that never empties is one nobody opens. The status-only park added two versions ago is gone; it solved half of this for one hand-off, and the queue makes that guard the rule for all of them.

A standalone photo bookmarklet, for sharing. Shows the Wex product photos for whatever item is open in NOVA, under the product name, with a full-size viewer on click — and nothing else. No front/side marking, no save, no messages to any app, because those only mean something against your own gear database. It depends on nothing else in the toolchain, so it works for someone who has none of it. Verified on a real inspection: photos found, the viewer steps and closes, every injected button is type="button", nothing carries a name, and the form’s own payload is unchanged at 84 fields with it present. It is in the repo as NOVA photos bookmarklet.txt, with install and troubleshooting notes written for someone who has never seen it.

v1.8.616

The reserved column was the wrong answer; the chip goes in the navbar, and video kit is graded on hours

The gutter is gone. v1.8.613 reserved a column so the panel had somewhere to be. Measured on the quote page it looked clean — no overflow, content reflowing to 693px — and that was the wrong measurement. Your theme positions the inspection form with width-tuned selectors, so taking 392px out from under it put Dust Level and Sensor Quality behind their own inputs and squeezed the accessory table past reading. The premise was wrong as well: on a 1080×1751 screen width is the scarce dimension and height is not, and the gutter spent the scarce one.

Nothing stamps data-pf any more, so any rule already pasted into the theme simply stops matching — there is nothing to undo mid-inspection. Delete those blocks whenever it suits.

The chip is in the navbar, and the card folds out over the form. v1.8.531 tried this and the chip landed at x=1317 in a 1080px viewport, so it gave up and floated instead. Measured properly this time: #navbarCollapse is a nowrap flex row whose third child’s box runs to 1309 while the things it actually paints stop at 447. Appending in flow either overflows or squeezes NOVA’s own controls, which are already compressed.

So the chip is positioned absolutely inside <nav>, out of the flex flow entirely, in the widest gap between elements that are genuinely drawn — measuring leaves, not containers, which is what produced “there are no gaps” on a navbar that is two-thirds empty. It is then checked against that gap as well as the viewport: sitting on top of the account menu is as useless as being off-screen, and looks like a bug rather than a fallback. The card now starts folded and opens from the chip, but only when the chip actually mounted — with no chip the count would be shown nowhere at all.

Video kit is graded on hours. NOVA has one Shutter Count box whatever the product is, and on a camcorder the number in it is recording hours — so it was being checked against shutter ceilings, comparing one unit against another’s limits. 1,200 hours is a grade 7 body; the shutter table has nothing to say about it. The bands come from the same Reference Grading table the sidebar shows, derived rather than retyped. The two are read differently and that matters: shutter is “<10K”, exclusive, so 10,000 is already over; hours is “0–150”, inclusive, so 150 is fine and 151 is over. Both boundaries are tested. Tests 120 → 131.

v1.8.615

The hand-off that only worked on the wrong screen, and an emoji heading for the Camera DB

Missing and Declined actually arrive now. v1.8.614 added the checker to the app’s boot path and not to the relay path — and the relay is the normal path. With the app already open on the other screen, the tab NOVA opens hands its query string to the tab that is alive and closes; that receiving tab ran seven checkers and not the new one. So the feature worked only when no other app tab was up, and failed in complete silence: no toast, no feed entry, the tab just closed. Both lists now have it, and a test asserts the two agree so the next hand-off cannot forget one.

The NEW marker is a coloured asterisk, and it stops travelling. Adding an accessory during an inspection makes NOVA prepend the NEW-button emoji into the name text — not a badge, not a class, just characters inside the same element every reader here uses. That means it could not be restyled from the theme (CSS cannot replace part of a text node), and, more to the point, the capture sends that same text onward into the Camera DB sheet’s “in the box” column. A row added mid-inspection could have written an emoji into a shared workbook, under a name that would never match the plain one again. One reader strips it now, the capture uses that reader, and the row is marked with an asterisk instead.

Declining an item that was already missing and already sent left the tracker wrong. The decline handler cleared missing and missingReason by hand instead of going through _setItemMissing, so no reversal was queued and the sent stamp stayed on the item — the tracker kept a −1 for something now recorded as declined. The un-mark button has gone through that helper since v1.8.468; this path never did. Found by reviewing the new hand-off, but it was reachable from the in-app button all along.

A status hand-off arriving while another dialog is open is now parked and offered when the screen is free, rather than replacing whatever was on it. Tests 110 → 120, and two of them exist because a mutation run showed the first versions passing while the bug was present — one asserted a helper instead of the function that calls it, the other let ordinary leading whitespace read as a marker, which would have asterisked every row.

v1.8.614

Missing and Declined reach the app, and three things quote 71639656 proved were wrong

Missing and Declined now reach Day Allocation. The last unmet line of the list. #chkMissing had never been read by the tool at all, and #chkDeclined was read for the panel’s own checks and never sent. Both now ride the same hand-off the gear capture already opens on save — a second window.open would have navigated the tab the first payload was still being read out of, since every hand-off targets the same named window.

It routes, it does not write. Marking something missing queues a −1 and the tracker tag depends on an answer only you have; declining needs a reason key from your curated list, and NOVA’s box holds free prose. So the app finds the line, confirms it is the right one, and hands it to the two modals that already ask those questions properly — with NOVA’s wording carried into the note field, not guessed at as a reason. Sent: quote ref, name, SKU, two booleans and that wording. Nothing else off a page that also carries a serial and a customer, and there is a test asserting exactly that.

A declined item stopped being told it owes a “Grade due to” sentence. The guard asked whether the decline REASON contained the word “declined”, which it essentially never does. Read off quote 71639656: the field is Declined Details: and its value is the reason itself — “Large piece of debris visible throughout the aperture range.” No match, so the guard never fired, and a declined Sony PXW-FS7 quoted at 9 and graded 6 was told the customer was owed a sentence. The signal is the field being filled in, not a word inside it.

The downgrade highlight stopped swallowing the missing accessory. NOVA concatenates its own segments onto the Inspection Notes cell with no separator, and the 28-135mm on that quote has no full stop in it at all: “Grade due to dust in the optics - this doesn’t affect images Missing Accessory: Sony ALC-F95S 95mm Front Lens Cap”. The end-of-sentence test ran to the end of the cell. It now also stops where NOVA’s next segment starts.

Rejected lines are no longer offered as work. The skip test asks whether a line has a usable View Inspection control, which was learnt from a quote where removed lines had theirs disabled. A rejected line disables nothing — all three on 71639656 carry a live link — so all three passed the test and “check these lines against today’s plan” offered to add them to the day. The state was being read correctly all along; nothing was asking for it. A comment in two files also claimed there is no “Rejected” badge, which was telling the next reader to delete the branch that handles it; that quote carries three.

The quote panel gets the same reserved column the pre-flight panel got. It is the same shape of problem — fixed, top-right, 360px, on a page whose content runs the full width — and shipping the gutter for one page only would have fixed half of it. One attribute, one rule, both pages. The “Last saved” block also gained the bottom margin it never had, being the last thing in its column. Tests: 84 → 110.

v1.8.613

The whole product name, and a column of the page that is finally yours

The Name field stops hiding the end of the name. #ProductName is an <input>, and an input clips at one line whatever width you give it — so there was no width that made this always right, only widths that made it wrong less often. The value itself was never truncated, only its rendering, so the tool now writes the whole name out underneath the field, wrapped. It appears only when the field is actually clipping (scrollWidth > clientWidth), so a name that already fits gains no extra furniture, and it follows what you type. It sits after the field’s form group, never inside span.twitter-typeahead — the wrapper that caused the v1.8.600 bug.

The pre-flight panel stops sitting on the form. Measured on the copy at your own viewport, portrait, with the columns stacked the way yours stack: the form runs the full width with no margin at all, so a 340px panel pinned to the right covers the right third of every field it is in front of — the Name and Serial Number boxes at rest, and more of the page as the findings list grows. It was never badly placed; it had nowhere to go.

Bottom-docking was the other candidate and does not work on a portrait screen: stacked, the form is over 2000px tall and scrolls, so a bottom strip covers live fields too, just a different edge. So the page now gets a column reserved for the panel instead. Layout is the theme’s job and the theme cannot see whether the panel is open, so the tool publishes what it is doing — data-pf on <html>, reading open, collapsed or stub — and one appended Stylebot rule reserves the width. Stow the panel and the width comes straight back; disarm it and the attribute goes with everything else.

v1.8.612

The NOVA copy now has NOVA’s shape, not just its field names

The copy had the right control ids and the wrong nesting, which made it convincing and wrong: your Stylebot theme positions the form with selectors like col-xl-7 > div:nth-child(9), so against a flat copy almost none of your theme applied and it showed my layout instead of yours.

It now reproduces the real structure — three rows, the col-7 / col-5 identity split at the top, the twelve field groups of col-xl-7 in order, the pairs that share a row (Category with Dust Level, Grade with Sensor Quality, Shutter with Firmware), the status checkboxes as direct children of their labels (your theme selects label:has(> #chkMissing)), both notes boxes as separate groups with the auto one readonly, and the White Box dialog in its real home in row 3.

The file, the behaviours and the bookmarklet are now generated from one source, because they had already drifted once: a single fix had to be applied three times by hand. And there’s a test that reads the generated markup and fails if the shape moves — verified by renaming one field and watching it fail.

v1.8.611

The downgrade check works on the item page, two batteries stop sharing one ledger, and the data channel stops opening tabs

The downgrade check now asks what the quote page asks. It only fired when the Inspection Notes were completely empty, so any sentence at all silenced it — including the grey-import line the tool types itself. It now uses your own rule (“Grade due to”) and gives the same two answers the quote page does: nothing written, or written about something else. It also looks the quoted condition up under the item’s original name, so a swapped item — the kind most likely to need a sentence — stops going silent.

Two batteries in one kit no longer share a ledger. The once-per-arm guards were keyed by accessory name, so the second row called “Battery” got no health buttons and inherited the first row’s state. Keyed by the row’s own input id now.

The data channel backs off. It asked for fresh data whenever the cache was stale — so if the app never answered (signed out, pop-up blocked), the cache stayed stale and it opened a window on every arm, all day, in exactly the situation where nothing arrives to explain why. Fifteen minutes between asks now, shared across tabs; “refresh now” still ignores it, because that one is you asking.

And a gap in the tests, found by them failing to catch a real bug I’d just written: GRADE_DUE_RE was used in one file and defined in another. node --check accepts that, and the fixtures missed it because the branch only runs when a quote page has parked a condition. There’s now a check that every ALL_CAPS constant exists in the file that uses it — verified by putting the bug back and watching it fail.

v1.8.610

I was wrong an hour ago — two manuals share a version number

v1.8.607 “fixed” the third-party and Figure 8 wordings by quoting the manual in the repo. You then pasted the current one, and it says the opposite: 3rd Party with no suffix, and See Spares on the charger with the lead listed in the Spares box. The version numbers are identical — both are v1.1 — so nothing about the older file announced that it was stale.

That wording has now been changed three times in two days by two readers working from two documents. So the current text is saved in the repo as nova-manual-current.txt, dated, with the two known differences written at the top, and the code says not to change an approved wording from any document that isn’t dated.

Picked up from the current manual while I was there: Figure 8 now covers Kettle leads and lighting heads too, and the in-store route lists the lead under #NOTINCLUDED# instead. The Guide, the phrase library and the checks all say that now. The Canon battery note goes back to the manual’s literal X/X — the buttons stay out of 3, which is your answer, and the sentence is a fill-in either way.

v1.8.609

Five answers, five changes

The battery comment is left empty. It used to type “X/4” into a customer-facing comment before you’d read anything, then block the save on its own placeholder — and the only permanent way out recorded that battery part as having no readout forever. Nothing is typed now; the one-press buttons appear on an empty comment exactly as they did on a placeholder.

Canon is out of 3. It had been sitting in the “scale unknown” group as a bare X/X, which is why the brand you see most was the one brand that never got buttons. Canon batteries now get 0/3–3/3.

Declining skips Grade, Dust and Sensor. So all of those checks are skipped on a declined item. Before this, a declined body with a leftover grade could have its save blocked over a shutter count — the tool arguing with a decision you’d already made.

The gear facts get a second chance. You arm on page load, which on a new product is exactly when Category is still blank — so every non-camera auto-tick was evaluated at the one moment the answer was unknown, and never again. They now re-run when Category goes from blank to set: once, and never on a later correction, because changing Lens to Camera afterwards is you correcting yourself and re-ticking underneath that would be overruling you.

The battery door no longer tells you to deduct anything. All five brands, but as you put it — admin makes the deduction, not you. It just says the door is recorded and who needs the cost.

v1.8.608

A copy of NOVA you can open from a bookmark

Guide section 11 has a second bookmarklet now: NOVA copy. Click it on the app (or any page that isn’t NOVA) and it replaces that page with a working copy of the inspection form — the real field ids, the real Grade / Dust / Category / accessory lists, the real White Box dialog, an invented camera and a nonsense serial. Then click NOVA tools on top of it and everything arms exactly as it would on a real item.

It’s delivered as a bookmark for the same reason the tools are: nothing has to be hosted, so nothing about NOVA is ever published anywhere. It refuses to run on wexphotovideo.com and says why — it replaces the page it runs on, and doing that to a real inspection mid-job would be alarming even though it can write nothing.

The point is that every interesting thing the tools do is a write on a live inspection: ticking Received, typing a note, adding an accessory, answering the packaging dialog, pressing Save. There has been nowhere to try any of it, which is why the last few interface faults were found by you rather than by me.

v1.8.607

The tool was blocking the manual’s own wording

Two save-blocking checks were demanding the wrong text, and v1.8.540 is where I broke them. That version shortened 3rd Party (Unbranded) to 3rd Party and replaced Missing Figure 8 Lead with See Spares, each time on the stated grounds that the longer wording “appears nowhere in the document”.

Both appear in the document. The manual says “Add the following to the Comments field: 3rd Party (Unbranded) — Please use this wording exactly as shown”, and then lists the variations it does not want. I read that list of forbidden variations as the rule and dropped the suffix sitting directly above it. The Figure 8 wording appears three times, including as a numbered instruction and again in the summary table.

Because these are exact-string FIX checks, the effect was that typing what the manual demands got your save refused — while the Guide tab went on teaching the full wording. Both are reverted, with the manual quoted verbatim in the code so it can’t be “corrected” again without reading the paragraph. The Figure 8 check now accepts either documented answer, because the manual has a second in-store route and a blocking check shouldn’t pick a winner between two correct ones.

v1.8.606

The battery x/x buttons wrap, and there is now a copy of NOVA to test on

A denominator of 4 puts six buttons in a row inside an accessory table cell. They were in a flex row that couldn’t wrap, so the cell demanded the width of all six at once, the table gave it, and the other column collapsed to a strip. They wrap now.

The bigger change is tools/mock: a copy of NOVA’s inspection page. Every behaviour worth testing — ticking Received, typing a note, adding an accessory, answering the White Box dialog, pressing Save — is a write on a live inspection, so there has been nowhere to try any of it. That is why the last three interface bugs were found by you and not by me.

It is built from a manifest read off the real page in view mode: the same control ids, the same Grade / Sensor / Dust / Category / accessory-type lists, the same five-column accessory table, and NOVA’s White Box dialog copied down to its onclick names. “Add another” adds its row asynchronously on purpose, because the real one does and the tool polls for it. Invented product, invented SKUs, nonsense serial, and no way to reach NOVA at all.

v1.8.605

Why the removed line never collapsed — and the packaging question is one dialog now

It was never your bookmark. I said that twice and I was wrong twice. markLineStates(rows) was called 46 lines above the var STATE_STYLE = {…} it depends on, and var hoists the declaration without the value — so at that moment it was undefined, and reading a colour off it threw. The row had already been stamped “handled” one line earlier, which is why it looked half-done, and every later run skipped it because of that stamp. The feature has never once worked on your screen.

Three things changed. The definition moved above the call. The stamp now goes on only when the work is actually finished, and the guard asks whether the strip is really there rather than whether we have been past this row before — so an interrupted pass repairs itself instead of sticking. And one row that throws no longer takes the rest of the pass down with it.

This is the third bug of exactly this shape (RULES, dids, now this one), so there is now a test for the shape itself: it walks every top-level call in the tool sources, follows what those functions reach into, and fails if any of it needs a var assigned further down the file. Re-introducing the bug makes it fail with the file and line; node --check never could, because the name does exist.

Separately, the packaging question is one dialog instead of two. NOVA’s own “add a White Box?” modal now carries Brown Box and Padded Envelope alongside its Yes and No, so the answer is one click where it was asked. NOVA’s two buttons are untouched; ours sit between them, every one explicitly type="button" because that modal lives inside the inspection form. No means “none of these” now, and the old second prompt stays as the fallback if the dialog isn’t found.

v1.8.604

The collapsed removed line stops looking like the next item’s header

“The collapsed one looks like it’s attached to the item below it.” It did. The strip had near-equal gaps above and below and a hairline border that was invisible against black, so the eye grouped it downward — it read as a title bar for the next product rather than as the remains of its own.

It’s now bounded like the separate object it is: a red band down the left edge, a red-tinted wash, a border dark red enough to actually see, and real air on both sides — 26px above, 34px below — because it belongs to neither neighbour. The name is struck through and dimmed, and it says “not inspected” outright.

Two things only a live page could have told me: every one of those styles now goes on with !important, because your Stylebot theme beats a plain injected style and my first pass was silently reverting to NOVA’s own 12px margins; and “show” moved inline, because the theme paints the product id in the top-right corner and a right-aligned hint sat underneath it.

v1.8.603

The panel tells you when your bookmark is out of date

You reported the removed line still not collapsing, so I ran the shipped code against 71639839 live: the Canon EOS 1D X row detects as Removed, moves its content into a hidden container, and the block drops from full height to 37px. The code was right — the copy in your bookmarks bar was older than it.

That’s the third report today with the same cause, so rather than say it again the panel now says it. It compares its own version against the app’s and leads with “BOOKMARK IS 12 VERSIONS BEHIND — RE-COPY FROM GUIDE 11”, ahead of everything else, because a stale bookmark makes the rest of what the panel says unreliable too.

This is only possible because of the data channel: before it, a bookmark had no way to learn what the app had become since it was copied. Compared numerically so 1.8.99 doesn’t read as newer than 1.8.602, and it says nothing at all when it can’t tell.

v1.8.602

I was matching for a badge that doesn’t exist

You said removed lines weren’t collapsing, so I read the badges off four live quotes. The real vocabulary is Accepted, Removed, Sent Back, Declined — there is no “Rejected”, which is what I’d written the matcher for.

And the more interesting fault: a row carries more than one badge. A margin warning rides alongside the state — “Removed | Margin (GPM %) went below your authorised level…”. I was reading the first badge and hoping it was the state. It now reads them all and takes the first that names one, so a row whose warning happens to come first is no longer invisible.

Sent Back and Declined are handled properly now too: Sent Back carries the same warning as a rejected line, since if it turned up anyway it still has to be declined on the item; Declined says nothing further is needed. Seven assertions cover the vocabulary, including that the margin warning on its own is not a state.

v1.8.602

Battery readings are one press

You were right that it’s consistent per brand — and the tool already knew it. The templates carry the denominator (X/4 for Nikon, X/5 for Fujifilm), so typing “X/4” into the box and asking you to edit one character in the middle was asking for the half it already had.

Where the denominator is known you now get a row of buttons on the battery row — 0/4 1/4 2/4 3/4 4/4 — one press, with the empty and full ends coloured. Where it isn’t known there are two small boxes instead: Canon’s template is X/X, both unknown, and inventing Canon’s scale is exactly the sort of guess that ends up in front of a customer.

Enter in those boxes is intercepted, because a bare Enter in a text input submits the form — which on an inspection page saves it.

v1.8.601

Removed, rejected and pending lines on the quote

Removed lines collapse to a single strip — state tag, name, and a click to expand. They aren’t work, and on a quote with several they were most of the page. Rejected lines get a red bar and the thing you actually have to do: if it turned up anyway, it has to be declined on the item. Pending ones get amber and say they weren’t actioned by admin — accept or reject.

State comes from the badge NOVA already renders per product. The vocabulary is matched loosely on purpose, and a state it doesn’t recognise leaves the row exactly as NOVA drew it rather than guessing. Accepted lines are untouched, since that’s the normal case and doesn’t need marking.

Nothing here changes NOVA’s data — it tints a row, adds a note, and folds one away. Every decision stays yours.

v1.8.601

Three real faults in the photo feature, from a review pass

The viewer could break arrow keys and Escape for the whole page. Its keyboard handler was only detached by closing the viewer properly — but re-arming the tool removes the viewer without that, and the handler captures both arrows and Escape at document level. After that, arrows wouldn’t move the caret in any NOVA field and Escape stopped closing NOVA’s own dropdowns, with nothing to explain it. Since the viewer covers the panel’s close button, clicking the bookmark again is exactly what you’d have done.

Saved marks vanished on the next arm. They’re read from data refreshed every six hours, so a mark saved now was absent until then — it would have read as “it didn’t save”. They’re held for the session now, the same way remembered spares are.

And a SKU with letters in it was being cleaned rather than refused — “12345A” became “12345”, a real and unrelated product, whose photos would have appeared with nothing to say they were wrong. It refuses now.

The save-safety review came back clean, which is the part that mattered most.

v1.8.600

The photos were being injected inside NOVA’s autocomplete widget

You couldn’t click them, so I went and looked on a live inspection page. #ProductName’s parent is <span class="twitter-typeahead"> — NOVA wraps that field in an autocomplete widget, and the photo strip was going inside it. That subtree belongs to the widget: it re-renders it, positions it and handles pointer events over it, so anything placed there behaves unpredictably.

The strip now climbs out to the field’s form-group and sits after it — verified on the live page that the new spot is outside the widget and the old one was inside. It falls back to the original position if NOVA ever drops the wrapper, since an awkwardly placed strip beats none.

Also found while looking: the Canon EOS R6 Mark II has seven views and I was only probing six, so the last was silently dropped. It probes ten now and keeps every hit rather than stopping at the first gap, because nothing guarantees the numbering has no holes.

And to be clear on the interaction, since it changed: it’s a single click on a photo to open the full-size viewer now, not a double click. Double-clicking was the first version and was never good — it’s undiscoverable and it fought the selection buttons underneath.

v1.8.599

No, the images can’t taint a save — measured, and now guarded

Your question was the right one to ask. Tested on the live inspection form: this tool’s markup does sit inside NOVA’s form, and the submitted data has 40 fields with it present and 40 without — nothing added. Images and layout elements aren’t form controls; only named inputs are.

There was one way it could have gone wrong, though: a bare <button> inside a form defaults to type=submit, and on an inspection page that saves the inspection. All eleven buttons the tools create are typed correctly — but nothing was holding that. Now a test checks every one and fails by line number if any is missed, verified by removing a type and watching it name the line.

v1.8.599

The photo picker, rebuilt

The first cut cycled one control through front → side → unset and sent a hand-off on every press — three clicks to correct a mistake, and a tab flicker each time. Each photo now has two named buttons, front and side, showing what’s currently set; picking a view for one role releases it from the other, so you can’t leave a contradiction on screen. Changes are held until Save marks, which only lights up when there’s something to save.

And a full-size viewer: click any photo for an overlay with prev/next, arrow keys, Escape or a backdrop click to close. Clicking the photo itself doesn’t close it, because that’s what you do while looking at something. It’s an overlay rather than a new tab so comparing a photo with the item on the bench doesn’t mean leaving the form.

v1.8.597

Product photos on the inspection sheet — and it costs nothing

The plan was a paid image search. Probing the real site made that unnecessary for most items, because of three things:

NOVA’s Parent SKU is Wex’s product id — SKU 1523925, read off a real inspection, resolves on Wex’s own asset server. The path is derivable: the SKU sits in a folder named for its own thousand. And extra views are the same name with -2, -3; both SKUs probed had four.

Your instinct about delisted stock was the important one. SKU 1523925’s product page is a 404 — long gone from the shop — while all four of its images still load. We’re reading the asset server, not the listing, so used gear that was pulled years ago still has its photographs.

So: no API, no key, no Cloud Function, no cost, and nothing leaves the page — your browser fetches an image from your employer’s own CDN, exactly as it would if you opened the product page yourself.

Photos appear beside the product name. Click one to mark it FRONT, click another for SIDE, double-click for full size. Only the choice is saved, per SKU, because the URLs derive from the SKU — storing them would record the same fact twice.

The gap, stated honestly: an item with no Parent SKU gets nothing here, and that’s exactly what a paid search would have filled. Now you can see how often it actually happens before deciding whether it’s worth paying for.

v1.8.596

One table of NOVA’s fields, and the panel says what it can’t find

The same handful of ids were named in seven separate maps — the forbidden-word selectors, their labels, the grade-cap sources, the placeholder boxes, the phrase-strip list, and the field-code lookup both ways. That’s why adding Sensor Specifics meant editing five of them and missing two, which left three grade caps matching nothing for twenty versions. There’s one manifest now and everything else is derived from it.

The part that matters more: a renamed NOVA field doesn’t throw an error. The tool reads through helpers that return nothing for a missing element — so a rename reads as an empty field, and “this field is empty” is exactly what several checks are about. Rename the Inspection Notes and every grey-import item would start blocking its own save with “the 5% line is not in the Inspection Notes”, with nothing to say why.

All 14 fields are now checked when the tool arms, and anything missing is named in the panel header: “1 FIELD NOT FOUND: the Inspection Notes”. That’s the one gap the test suite explicitly couldn’t cover, since fixtures are my model of NOVA rather than NOVA itself. Four new assertions, verified by removing a field and watching it get named.

v1.8.595

A throw in the hand-off path could have blanked the whole app

The hand-off and bundle checks run at the very top of the app, above the sign-in call and the auth handler. They weren’t wrapped — so a throw in either (a malformed query string, storage blocked by a browser setting, an oddity reading the opener) would take the rest of the module with it, and you’d get a blank screen with no way to sign in.

Every version of this file has had that shape; the bundle server added a second route into it, which is what made it worth finding. It fails open now: any failure there logs and boots normally. Same discipline the tools have always used — a check that can wedge Save is worse than no check, and a hand-off that can wedge the whole app is worse still.

v1.8.594

The wrapping on 71634674 was mine

Measured on that quote: “30W power supply with locking connector” rendered as a 458px pill inside a 462px column. My pills carried white-space: nowrap, so a long accessory name couldn’t break — it filled the row and clipped. Long names now wrap inside their own pill; short ones still sit on one line because they fit.

While measuring I also found NOVA’s own labels truncating at your window width — “Amount Quoted:” needs 145px and gets 102px, so it renders with an ellipsis. That one isn’t fixable from a bookmarklet, because your Stylebot theme outranks anything injected. It wants a rule in the theme.

v1.8.594

Four things that made the panel harder to work next to

The tool’s receipts were piling into your customer-facing box. Every “I did this for you” note was anchored to the Inspection Notes whatever it was about, and they concatenate — so after a normal item that field carried a paragraph of the tool’s own admin. Each now names the field it actually touched, and only the most recent keeps an inline mark. The panel still lists them all.

The suggestion strip and the sentence pills kept trading places. Both inserted immediately after the field, on different timers — the strip on a keystroke, the pills on the settled pass — so they swapped as you typed. Everything now lives in one container per field, in a fixed order.

“Neither” is an answer. Answering it on the packaging prompt only closed the box, so the panel went on pointing you at a White Box popup that no longer exists, for the rest of the item. It records the answer now and says what it means instead.

And the battery-door note was beating you to the tick. A row added via Add another arrives unticked, and the note was typed 300ms later — into the customer-facing box, add-only, so it never came back out. The manual’s own process is to add that row in order to tick it. It now waits for a second settled pass, so ticking it means nothing is written.

v1.8.593

The bookmarklet asks for its data instead of carrying a frozen copy

Until now everything the tools knew — the manual’s rules, your phrase library, the gear index, your scored errors — was baked in when you last copied the bookmark. That’s why every “remember this” feature has been half-built: a battery marked today, a spares wording saved against a product name, a phrase reworded this morning, none of it reached the tool until you re-copied. Each one had grown its own workaround to compensate.

The tool now opens this app at a bundle endpoint, and the app answers over postMessage to NOVA’s origin and closes itself. No server, no public endpoint, no sign-in — it’s the same data going to the same page as before, by a route that can be taken again tomorrow. It’s cached on NOVA’s side and refreshed when it’s more than six hours old, with a Refresh data button for the moment right after you’ve changed something.

The answer lands after the current item, deliberately. Nothing waits on a round trip in front of an inspection, so a refresh is in place from the next time you arm the tool rather than this one.

Two things worth knowing. It can only add freshness, never take function away — a blocked pop-up, a signed-out app or unavailable storage all leave the baked data in place, exactly as every version before this worked. And the panel now shows how old the data is rather than when the bookmark was copied, which is the question that actually matters: the old staleness warning counted gear-DB rows, so a reworded phrase never read as stale while a gear count change did.

The origin check is the whole security boundary of the channel, so it’s asserted in the test suite — and I verified it’s load-bearing by weakening it and watching the tests fail. 44 checks now, up from 31.

v1.8.592

The NOVA tools have tests now

31 checks, run with node tools/test/run.cjs, holding the judgements that reach a customer: a decline with no reason blocks the save; an unfilled placeholder is caught in all four boxes; a Brown Box is recognised even when the bookmark carries no packaging config; SEE SPARES counts on any accessory row; a Battery Door isn’t a battery; a semicolon doesn’t split a sentence. Each of those was argued into its behaviour after getting it wrong once, and nothing was holding them there.

The one that earns its keep calls render(). Twice now a reference to an identifier that doesn’t exist has shipped — most recently the bug that drew the panel as an empty line. node --check passes those: a free variable is valid syntax. I proved the gap by re-injecting that exact bug — the syntax check waved it through, and the suite named it in three places.

No test library, and no dependencies added. The tool reads the page through exactly one seam, so a lookup table keyed by selector stands in for a live form. Nothing is exported for testing from the shipped source, so none of this adds bytes to the bookmarklet.

It runs in CI before any deploy — though that workflow file still needs committing by you, along with yesterday’s gate.

v1.8.591

An architecture pass, and the defects it counted

A structural review of the NOVA tools — 6,581 lines, 174KB of built code, plus a data prelude. Its findings are below; these are the ones cheap and certain enough to fix straight away, and each was found by counting rather than reading, which is the kind of defect a normal review walks past.

Seven of eight hand-off receivers guard against viewer mode. One didn’t — the tick, which writes state. A manager in viewer mode following a tick link would have had it applied against data they were only supposed to be reading. Eight hand-rolled receivers is how a rule gets applied seven times.

Three of nine tab-opening calls checked whether the tab opened. The rest reported “Sent — answer in the app” whether or not a pop-up was blocked. They all check now, and the quote-page buttons say “Blocked — allow pop-ups and try again” instead of lying.

And the build had a mode that could only give a wrong answer: building a single tool targeted constants this app hasn’t had in months, so its freshness check could only ever report out-of-sync. Gone.

v1.8.591

A deploy gate, waiting on you to commit it

The tools are source; what ships is a string pasted into the app. Edit a tool and forget to rebuild, and the repo looks right while the bookmark is a version behind — with nothing to reveal it until something misbehaves on a live page. Two five-second gates before publishing fix that: a syntax check, and a check that the pasted bookmarklet matches its source. Verified by watching it fail on real drift, then pass once rebuilt.

The change is written and sitting in the working tree, but GitHub refuses a workflow edit from this session — it needs the workflow scope. Commit and push that one file yourself and it takes effect.

v1.8.590

Removed lines aren’t lines

You said removed lines don’t get inspected — greyed out, View and Send Back disabled. Looking at 71635700 turned up a second shape as well: three blocks with no controls at all, named “Litemons” and “litemons”, carrying the same class as a real product. The tool was counting all of them as products.

That mattered most for the new line check: it would have offered “Litemons” to today’s plan as an item to add — putting work in your day that will never happen. It was also inflating the panel’s count, asking the history for saves that can’t exist, and drawing pills on blocks that aren’t items.

The test is the View Inspection control: present and usable means a line someone will inspect. Disabled means removed. Absent entirely, along with Start and Edit, means it was never a line. Everything else — the checks, the counts, the history read, the pills, the per-item fetches and the line diff — now works from the inspectable ones only.

v1.8.589

It remembered a single bullet

Read out of your browser rather than guessed at again. The remembered wording for the LA200R was one character — a bullet — with no accessory rows. You’d started typing the list on the second light, the box held just , and at that moment it was non-empty with nothing remembered yet, so the button offered to remember it and did exactly what it was told. Filling from that wrote one bullet into an empty box, which is indistinguishable from nothing happening.

My fault twice: no floor on what could be remembered, and a fill that couldn’t tell writing a character from writing a list. There’s a minimum now — bullets and whitespace don’t count towards it, so a box holding only list punctuation is worth nothing however many lines it runs to. It’s enforced on the way in and on the way out, so the junk already saved stops being offered without you having to clear anything.

v1.8.589

Never “start inspection” to read an item

Your rule, now enforced twice over. The quote tool reads each item’s page to get its Spares, accessories, serial and SKU, and it already preferred View Inspection and excluded the start inspection link by its id. It now also refuses any link carrying updateStatus=true, so a renamed id can’t quietly turn reading a field into starting an inspection.

v1.8.588

The inline buttons could lose your press, and never said so

Two causes, and rather than guess between them both are fixed.

They fired on click, which needs a press and a release on the same element. Pressing one moves focus off the field, and which element has focus is part of what decides whether the tool rebuilds its inline furniture — so the button could be torn down and rebuilt between the two halves of your press, and the click would never arrive. The suggestion chips have used mousedown for exactly this since they were written; the newer buttons were never given the same treatment. They all use it now, and they answer the keyboard too.

And they only reported into the panel — specifically the “Done for you” group, which is collapsed by default and gone entirely when the panel is stowed. So a button that worked silently looked exactly like a button that did nothing. Each one now leaves a short line beside itself saying what it did: “Filled — and set 1 accessory row”. The panel stays the log; this is the receipt.

v1.8.587

Filling from last time now sets the accessory rows too

Remembering an item now captures its accessory rows as well as the Spares text — which were ticked, and what their comments said. Filling replays them, so the second of two identical lights gets its Original Box ticked Received with SEE SPARES on it, not just the spares block.

Add-only, and that is the whole design. A Received tick is a claim about what physically arrived in front of you, and copying one from a different unit asserts something the tool cannot know. So it never unticks anything, never overwrites a comment you have written, and never touches a row you have already answered either way — a row you marked Replace keeps Replace and only gains the missing note. It fills blanks, and it names every blank it filled.

The tooltip shows the whole thing before you press it: the spares text, each accessory it would touch and what it would set. Only rows that carry something are remembered — a name with no tick and no comment replays nothing — and a row the old item had that this one doesn’t is simply skipped, because adding accessory rows changes what the customer is told to expect.

v1.8.586

The page was fighting you as you typed

Every settled change rebuilt all of the tool’s inline furniture — the suggestion strip, sentence pills under four boxes, a marker button per accessory row, and the kit / serial / battery controls. Since v1.8.560 that watcher listens on the whole document, so a keystroke anywhere tore the lot out and put it back. Removing and re-adding elements above the viewport moves the page under your cursor, and doing it every 300ms while typing is the scrolling you were seeing.

It now rebuilds only when something it actually reads has changed — the four notes boxes, the Spares box, name, SKU, serial, which box has focus, and the accessory rows. Unchanged means the result would be identical, so building it again could only do harm.

Also trimmed: “+ SEE SPARES” is one button, not one per row. Offering it on every accessory with an empty comment put eight of them on a well-specified body. It goes on the packaging row, which is where the manual’s own example puts the link, or the first row that can take it — and you can always type it on a different row yourself.

v1.8.584

Remembered spares work from the very next item

Tested the feature against the real page rather than assuming. The data path is exact: what you type is what comes back, a different product name returns nothing, casing and stray spaces still match, and re-remembering the same item updates its entry rather than adding a second. All six button states behave — nothing at all when the item has a SKU (the gear DB’s job), “Remember these spares” when it doesn’t, “Update” when the wording has changed, “Fill from last time” on an empty box, and silence when there’s nothing to say.

That testing found one real gap: the memory only reached the bookmark on a re-copy, so remembering a wording did nothing for the rest of the day you used it — and a lighting kit is exactly the thing that comes through three times in a morning. The wording is now also kept for the current browsing session, so it’s offered back on the very next item, with the app’s copy remaining the durable one.

Session storage rather than local, deliberately: NOVA’s local storage gets wiped, which is what moved the battery-health memory into this app in the first place. A longer promise kept there is one that quietly breaks.

v1.8.582

Spares remembered by name, for the items with no SKU

Fill the Spares box on a product with no Parent SKU and a button offers to remember that wording. Next time the same name turns up, still without a SKU, it offers to fill it back in.

Only without a SKU, and that’s the point: with one, the gear DB already knows what a model ships with and keys it properly. Without one the name is the only handle there is — a weaker one, since a product can be written two ways and two products can read alike. So it offers in both directions and never writes on its own, and the tooltip shows you the remembered text before you take it.

The memory lives in this app rather than in the browser on NOVA’s side, because NOVA’s local storage gets wiped — that’s what moved the battery-health memory here in the first place, and a promise kept there is one that quietly breaks. The cost is the usual one: a wording remembered today reaches the bookmark the next time you re-copy it.

v1.8.581

A quote’s lines, checked against today’s plan

The last structural gap between NOVA and this app, and the oldest line still open on your list. Add a line to a quote and nothing ever told this app about it — the only thing that sent an item required a swap, which a new line never is.

The quote panel now has a button that sends the product names and SKUs on that quote. The comparison happens here, not in the bookmarklet, and that isn’t an implementation detail: the bookmark is a frozen snapshot from when you copied it, so today’s plan baked into it would be wrong by tomorrow morning. The tool reports what’s on the page; this app knows the plan and owns every consequence of changing it.

“Not the item quoted” is handled in the same pass. A swapped line isn’t missing from your plan — your plan has it wrong, because it still holds what the customer was quoted while NOVA holds what turned up. Those are found by the quoted name and handed to the substitute flow, which already knows about tracker reversals and the “quoted as” record.

It also tells you about lines in your plan that are no longer on the quote — and deliberately does nothing about them. Removing work from a day has tracker consequences that deserve a decision of their own, not a confirm buried under two others.

A button rather than automatic: it opens a tab, and doing that on every quote page you land on is the interruption the tab relay was built to stop. Nothing is added without you confirming it here, and everything goes through the same path a manual add uses, so the changes log, the allocation snapshot and the tracker’s pending writes all see it.

v1.8.580

Two things were writing untruths into customer-facing boxes

A fresh-eyes review of all four free-text fields. Worst first.

“Excellent” was typed into every GoPro battery comment as a fact. The battery-health templates are fill-in-the-blank figures — X/4, X/5, X/X — but GoPro’s is the bare word Excellent, with no blank in it. So the tool asserted the best possible battery health, with no reading taken, in a comment the customer sees, and the unfilled-placeholder gate couldn’t catch it because there was nothing to fill. It now refuses to type any template that doesn’t visibly want finishing. GoPro batteries will now ask you for the wording — tell me how you phrase it and I’ll add it properly.

Two seeded phrases were wrong. One read “Battery is bugling” — the manual’s typo, corrected elsewhere in the app but not here. The other, a decline reason, read “the xxxxx button not working”; the gate looked for a bare “xx” and let five through, so it could reach a refused customer verbatim. Both corrected in your library, not just the defaults.

And three sensor grade caps have matched nothing since v1.8.571, when the sensor wordings moved to their own box and took them out of view of the cap checker.

v1.8.580

The sentence pills could corrupt what they were splitting

Yesterday’s pills split on any of . ; ! ?. Three ways that goes wrong, all reaching a customer: the library’s own shape is “There is light moisture within the optics; however, this does not affect image quality” — two pills, and deleting either leaves a sentence that means something different. f/3.5-5.6, approx. 2mm and e.g. all split mid-word. And removing a pill flattened every line break in the field.

Now a terminator only ends a sentence when whitespace and a capital follow, or the text ends; a newline always does. The delete re-derives its range from the live text at click rather than trusting an offset from up to 300ms ago, and collapses only the join. Checked against all of those cases.

Two more in the same area: a struck-through chip was still clickable, so it doubled as an insert-a-second-copy button; and clicking a chip after moving the caret spliced at the old position and mangled the line — it re-asks at the moment of the click now.

v1.8.580

SEE SPARES and #NOTINCLUDED#, as buttons

The tool polices both — it asks for the link when the Spares box has content, and blocks a save on a near-miss of the marker — but the one thing it couldn’t do was type them. Both are internal shorthand, deliberately kept out of the phrase suggestions, which is exactly why they needed their own way in.

+ #NOTINCLUDED# sits by the Spares box when it has content and no marker, and goes in on its own line, because what follows it is read one accessory per line. + SEE SPARES appears on accessory rows with an empty comment while the link is missing — so it never proposes overwriting your wording, and the moment you use one they all disappear.

v1.8.580

Chips now end the previous sentence

Inserting two phrases produced “…to rubber grips Light internal dust within the optics” — one sentence as far as every part of the tool was concerned. The pills drew no boundary, the filter treated the whole run as what you were reaching for, and the per-clause grade-cap matching read two faults as one clause. The tool was defeating its own sentence model with its own output. It adds the full stop now, and capitalises what follows.

v1.8.578

The decline reason is a fourth customer-facing box, and the tool didn’t know it existed

Confirmed off a real declined item — no clicking, nothing changed: the reason lives in #txtDeclineNote, a plain textarea with no pick-list. Whatever is typed there is exactly what the customer is told about the refusal.

The “write the reason” prompt was watching the wrong box. It asked whether the Inspection Notes were empty — a different box with a different audience. So it nagged when the reason was already written, and, far worse, stayed silent on a decline whose reason box was empty as long as anything at all sat in the Inspection Notes. And when you took the offer, it wrote the reason into the Inspection Notes, where the customer reading about their refusal would never see it.

Now: the prompt watches the decline box, the decline wordings are tagged to it and route there, and declining with no reason written stops the save. That’s the one omission here you can’t recover later — the item goes back and the customer is told nothing.

It also joins the other three properly: suggestions attach to it, its contents show as sentence pills, and the unfilled-placeholder gate covers it.

v1.8.577

A Brown Box was invisible to the tool that asked you to add one

Recognising a replacement box matched only the names in the rules baked into your bookmark, plus a hardcoded White Box. A bookmark copied before those options existed carries none — so a Brown Box row was invisible: the panel kept saying “answer the White Box popup” with the answer sitting in the table, and the auto-tick never fired because it asks the same question through the same matcher.

A brown box is a brown box whatever the config says. The known kinds are matched outright now and the rules only ever add to them, compared on a flattened name so a stray double space or casing can’t decide it. Same flattening on “has this already been added”, which otherwise reads a spacing difference as “no” and adds it twice.

v1.8.577

Every sentence in a notes box is a pill you can remove

These boxes accumulate sentences joined by spaces, so by the third one it’s a wall of prose and deleting the second means selecting it by hand without clipping the ones either side. Each box now shows its contents as pills — click one to select that exact sentence in the field, or press its × to remove it, which tidies the join so you don’t get a double space where it was.

All three boxes at once, not just the one you’re typing in: “what’s in the Unique Product Notes” is worth being able to see while writing the Inspection Notes, which is exactly when the two get mixed up. Clicking selects rather than opening an editor — the field is the editor, and a second one would just be somewhere else for the text to disagree with itself.

v1.8.576

“Grade due to” is answered by what you already wrote

Put “heavy peeling to the rubber of the grips” in the Unique Product Notes, type Grade due to in the Inspection Notes, and the grip-peeling wording is now the first chip. That’s the actual shape of the work — describe the fault for the buyer, then explain the grade by naming the same fault — so the strongest signal for “what is this grade due to” is sitting in the other box, in your words, already written. Far better than page context, which mentions everything the item has rather than what was wrong with it.

“Grade due to” is also no longer treated as a filter. As a substring it narrowed to the fifteen phrases whose own text contains those words — the ready-made whole sentences — and excluded every lowercase fragment written to follow it. It’s an opening now, stripped before the filter runs, so “Grade due to peel” searches for “peel”.

And a bug of mine this uncovered: v1.8.569 said Unique-Notes wordings were “reachable but not volunteered” in the Inspection Notes. They weren’t reachable at all — a hard field filter dropped them before the softer demotion could run, so the demotion was dead code and the “Grade due to ” + fragment path it claimed to protect never worked. Only genuinely impossible pairings are excluded outright now.

v1.8.576

The battery “no x/x” button sits on the battery row

We didn’t decide against it — it was built back in v1.8.528 and it does remember: the SKU goes to this app, which resolves it to the battery part, so every camera taking that battery is covered from then on. Nothing is written into NOVA.

But it was a button in the panel at the top right of the page, and only visible while the panel happened to be asking for a figure — while the row it’s about is down in the accessories table. It now sits beside that battery’s comment box, on any battery actually received, and disappears once the part is known.

v1.8.575

A “SEE SPARES” link means it came with the item

You’d already drawn this distinction once, and it was written down in the pre-flight’s own check: a Spares box with no link carries the quote’s own loose spares, which is what a spares label is for; a Spares box with a link is a bonus extra bundled with that product, recorded against the accessory it arrived with.

The label offer never looked at the link at all — it fired on the box being non-empty. So the one case that is definitively not loose spares was the case that set it off. It now skips a linked one, and labels its inline block “Spares (included with the item)” so you can see which reading it took.

The same fault, from the other end: the check that asks for the link only looked at Original Box and Packaging rows. On 61637174 the link sits on the Manual row — so it found none and raised the question on an item where the process had been followed exactly. Any accessory row counts now. The manual asks for the link; it doesn’t say which row must carry it, and the sensible row is whichever accessory the extras arrived with.

v1.8.574

Every serial control now sits with the serial field

Check serial was a button in the panel — top right of the page, while the number it’s about is in the middle of the form. Same argument as the phrase chips and “+ kit”: a control that acts on a field belongs beside that field. It, Mark serial checked, and the stand-in-serial button now share one slot next to the input, which works because their conditions are mutually exclusive — nothing typed offers the stand-in, a Canon serial offers the check, a check already opened offers “checked”.

One catch handled: when a page is all clear the tool disarms itself after a moment, and it decides that by asking what the panel still has to offer. The serial controls left the panel but not the page, so an all-clear Canon body would have torn down and taken the button with it.

v1.8.574

The spares label now says what set it off

Quote 61637174 offered a label with, as far as you could see, no spares on it. It had some: one product of four — the Canon CF-R20EP Cooling Fan Grip — carries “Terminal covers x 3” in its Spares box. The read was right and the offer still felt wrong, because it never said which product or what it had found, so a Spares box left filled by an earlier inspection looks identical to a real one.

It now names the product and quotes the text before the tab opens, and adds the obvious next step: if that box should be empty, clear it in NOVA, or it’ll offer a label again every time.

v1.8.573

Packaging split out on the left column too

Last release separated it in the inspection’s list on the right. The customer’s stated list on the left — the long comma-split run of pills — still had the box buried in the middle of it, which is where you were actually looking.

It now sits on its own line at the end under a small packaging heading, with Original Box first and whatever answers it after. Both columns read the same way now: accessories, then packaging. A box is a different kind of thing from a lens cap — it has its own White Box flow, Replace on it is correct rather than a fault, and it’s the last thing checked before a quote goes to Inspection Complete.

The heading is deliberately not a pill, so the inspection read that colours pills green/blue/red doesn’t treat it as an accessory that failed to turn up. And a list that’s only packaging gets no divider, since there’d be nothing above it to divide from.

v1.8.572

The suggestions were being ranked on the wrong words

The product name was in the scoring. “Canon EOS 5D Mark IV Body” offered all four sensor-debris phrases before you typed anything, because mark matches “small mark” — every Mark-anything body did it. A 3-Section tripod pinned lens-barrel decline wordings off section. A Godox V1 Flash got pop-up flash casing cracks. The name says what the item is; these phrases are about what’s wrong with it, and they share vocabulary by accident. It’s gone.

The curator’s label was being scored as if it were the sentence. That gave weight to “alt”, “wording”, “plain”, “ungraded”, “customer”, “friendly” — and to “no”. Which is how a page with Dust Level “light dust” came to pin Viewfinder · Debris only, no dust (light): it matched on the word “dust” inside a label saying the phrase is about its absence. Only the wording is scored now; labels still drive the typed filter, where a search box should match them.

And the pin ignored the category demotion — the one place that did. On a lens, the dust pin could be a viewfinder phrase. A pin is the strongest claim the strip makes, so it now knows everything the ordinary ranking knows.

Two more: short words (usb, lcd, iso, af) could never enter the ranking at all because the page used a four-letter floor against the phrase index’s two — “Ports · USB” scored zero from a note saying “usb”. And synonym folding was a substring swap, so “optics” became “opticss” and “scratches” became “scratcheses”; it folds on word boundaries now, as its own rules always claimed.

v1.8.572

Chips show the sentence, and the dust ladder has wordings

A chip was the label alone, with the actual sentence only in a hover tooltip — so mid-item you chose between “Viewfinder · Dust and debris, alt wording (light)” and “Viewfinder · Debris only, no dust (light)” without being shown either. A decision about prose, made from a filing name. The chip now shows the wording with the label small above it.

The Grade Wizard has all three rungs of the optics-dust ladder as faults (light / moderate / very heavy, at 9+ / 9− / 7) but the library had no customer sentence for any of them — which is what the dust pin was reaching for and failing to find. Three added. Worth your eye: they’re derived by analogy from your own optics idiom, not transcribed from the manual, which states the faults but no approved wording. Reword them in Reference Phrases if they’re not how you’d put it.

Also: a phrase already in the box wasn’t recognised as already there if the case or dash differed — and Unique Product Notes force-lowercases what you type, so that was routine. It would show no strike-through and happily append a second copy.

v1.8.572

Packaging listed on its own in the quote overview

It was left out of the right-hand pills entirely, because a missing Original Box is correctly marked Replace and would have shown as a red “not received” on every such item. But left out means invisible — and whether a brown box or a white box actually got added is the last thing to check before a quote goes to Inspection Complete.

It has its own row now, with Original Box first and whatever replaced it after. Replace on packaging is the correct state here, not a warning, so it never counts as missing and never turns the block red.

v1.8.571

A sentence with a hole in it can no longer reach a customer

The save gate has watched accessory comments for a leftover X/4 since v1.8.525, because an accessory comment shows on the website. The notes boxes are every bit as customer-facing and had no such check — while the suggestion chips insert these verbatim: “affects images from xx”, [f-stop], [item], [error code].

All three free-text boxes are now checked, and it blocks the save with “Save anyway” still there. front/rear is the awkward one and is matched deliberately: it’s a choice left in the text rather than a blank, so it reads as finished prose until you notice the slash.

v1.8.571

Sensor Specifics gets its own suggestions

The four sensor-debris wordings describe what the debris does to an image — which is the question NOVA’s Sensor Specifics box asks, not either notes box. The chips now attach to that field too, and those four are tagged to it. The pre-flight already checked that field was filled when sensor marks affect images; now it can help fill it.

v1.8.571

Two contradictions in the phrase library, settled

A live lens mite is a decline. Two phrases claimed different policies for the same fault — one graded it 6, one declined it. The grade-6 live wording is gone; the dead-mite grade-6 wording and the decline both stay.

“at ISO 100 from f11” was stated as fact in four wordings whose Inspection-Note counterparts say “from xx” for you to fill in. A customer whose fault starts at f8 would have been told f11. Those four now say xx — and the new gate catches one left unfilled.

Applied as a one-shot correction to your live library, not just the defaults: that seed ran long ago and editing it would change nothing. It only touches an entry still carrying the exact original text, so anything you’ve reworded is yours and is left alone.

v1.8.570

The “Grade due to” highlight never appeared

Two faults, both mine from last release. It gave up on any markup inside the notes cell — a guard meant to avoid rebuilding NOVA’s own DOM as flat text. But a note of more than one line contains a line break, so the guard fired on exactly the notes most likely to carry a downgrade explanation. It now walks the text nodes and wraps the match where it sits, preserving NOVA’s markup rather than avoiding it, which is what the guard actually wanted.

And your Stylebot theme outranks anything a script injects, so a plain inline style could be overruled by a broad rule on that cell. The highlight’s colour, border and background are now set as !important, which an inline style attribute can’t express.

v1.8.569

The suggestions went dead after the first phrase, every time

A fresh-eyes review pass found it, and it explains “the suggested phrases needs work” on its own. The filter took everything since the last line break as “what you’ve typed” — but every writer in the tool joins sentences with a space, never a newline. So the moment one phrase landed, “the line” was the whole box: nothing matched, the strip removed itself, and the chips never came back for that field. One chip per box per item was the practical limit.

It also made your own convention unusable. Type “Grade due to heavy peel” and the filter was that entire string; no phrase contains it; no chips. Sixty-odd wordings in the library are written to be the tail of “Grade due to ”, and there was no way to reach them that way.

The filter is now the longest suffix of the current sentence that some phrase actually contains. “Grade due to heavy peel” filters on “heavy peel”, and the chip completes the sentence without touching your opening. A suffix that’s already a whole phrase means you just inserted one, so it goes back to page-ranked suggestions instead of offering you the thing you picked.

v1.8.569

A decline reason could be filed where only a buyer would read it

Two decline reasons are about battery recharge performance, and the battery rule was tested before the decline rule — so both were tagged as Unique Product Notes. That isn’t academic: the panel’s Declined — write the reason button opens the picker seeded with “declin”, and those route to the buyer-facing box. The one flow built to put a decline in front of the customer could file it out of their sight.

And the picker was still sending every untagged phrase to the Inspection Notes whichever box you opened it from — the same bug the chips were fixed for, for three-quarters of the library. It now follows the box you came from.

v1.8.569

The library knows which box it belongs in

123 of 165 phrases were untagged and therefore offered in both boxes. Read one by one, the split wasn’t ambiguous — it just hadn’t been made: 108 are plainly condition facts for whoever buys the item. The battery wording you reported wasn’t a special case, it was one of a hundred.

Those are now reachable but not volunteered in the Inspection Notes. Both halves matter: not volunteered, because that’s how a buyer’s sentence ended up one click from the seller’s box; still reachable by typing, because “Grade due to ” plus one of those fragments is exactly how you write a downgrade.

Seven entries turned out to belong in neither box — SEE SPARES, #NOTINCLUDED#, REPLACED and the approved-replacement brand names. The tool contradicted itself on these: its own forbidden-words check flags them as internal shorthand when they appear in the customer-facing notes, while the chips were offering them there. They’re now excluded from both boxes and left to the picker.

v1.8.568

“Grade due to” is the rule, and it’s marked where you wrote it

You said your downgrade notes always say Grade due to. That settles a question this check had been guessing at twice: it demanded that opening, then was loosened to accept any note because the manual lists other wordings, then given a vocabulary of grade-ish words after 71635409 slipped through on a note about a USB cable. All inference. It’s your convention and your notes, so it now just reads it.

Still two findings, because “nothing written” and “written about something else” are different mistakes to have made.

And the phrase is highlighted where it sits, in the Inspection Notes on the quote overview — the run from “Grade due to” to the end of that sentence, so the rest of the note reads normally. The answer to “did I write it” belongs in the same glance as the note, not in a badge somewhere else. A textarea can’t hold a highlight, so on the item page it marks the field instead: grade due to — written.

v1.8.568

Check serial was never going to appear on a Canon body

The button’s conditions were worked out once, when you click the bookmarklet — and one of them is “a serial is present”. You arm the tool when the page opens and type the serial afterwards, so it was evaluated at the one moment it could never be true, and never asked again. It’s a live question now, re-asked on every change like everything else in the panel.

v1.8.568

A suggested phrase replaces what you were typing

Type “grade due t”, click the chip, and you got the full phrase appended after your half-written words. But that half-line is the filter that produced the chip — it’s you reaching for the phrase, not a sentence you wanted kept. It now replaces it, the way any other autocomplete would, and leaves the caret at the end. Only the line the caret is on; anything already written above is untouched.

v1.8.567

One missing argument, both symptoms

The console spam and the panel drawing as a bare line were the same bug. The “Done for you” list was worked out in one function and read in another, so every render threw dids is not defined partway through assembling the panel — before the content was ever put on screen, leaving an empty bordered div. Fixed by passing it.

And a guard so this class of mistake can’t look like that again: the checks have always failed open, but the drawing had no such protection, and it’s the half that can leave a wreck on someone else’s page. If it throws now, the panel says so and names the error, with the field markers and save check still running. A panel that says it broke is a bug you can report; a line is one you have to describe.

v1.8.567

Phrases know which box they belong in

Your point: Inspection Notes carry what the customer is told about the inspection; Unique Product Notes carry what someone buying the item needs to know. “The battery has full recharge performance remaining” is the second, and was landing in the first.

The picker was the direct cause — it wrote every phrase into the Inspection Notes regardless of which it was. It now writes into the box the wording belongs to. The suggestion chips already wrote wherever your cursor was; they now also stop offering a phrase that belongs in the other box.

Which box is derived, not hand-tagged across ninety-nine rows, and only where the library already says so plainly: the label naming the box (the Mite pair does this outright), “Grade due to…” being an Inspection Note by the pre-flight’s own existing rule, battery-health readings, and decline wordings. Everything else stays offered in both — guessing wrong would hide the right wording from the box you’re standing in, which is worse than showing one too many.

v1.8.566

Read off the real quote 71635409, and two things were wrong

The serial isn’t on the quote page at all. Last release read it by label there; the product blocks carry Accepted, Condition, Amount Quoted, Refusal Reason, Accessories, Cash Eligibility, Margin, Inspection Location, Grade, Cost and Inspection Notes, and nothing else. So it always found an empty string and rendered nothing. It now comes from the item’s own inspection page, on the fetch that already happens for Spares and accessories.

“Never saved” on an item with a save right there in the log. On that quote the history names the lens Canon EF 70-300mm f4-5.6 IS USM Lens while the product block calls it Canon EF 70-300mm f4.5-5.6 DO IS USM Lens — a genuinely different string, because the item on the bench isn’t the item quoted, and its own note says so. No amount of normalising joins those two.

So history names are now assigned rather than matched: each distinct name in the log is scored against each product on the quote by shared words, and the best pairing wins — but only on a clear win over the runner-up and at least three shared words, because two Canon lenses on one quote share “canon” and “lens” and nothing that tells them apart. A quote holds two to five products, so this is a small bounded problem with a right answer, not a fuzzy search. Checked against that quote’s real names: the body matches exactly, the swapped lens now finds its save, and the ungraded 18-55mm correctly still reports never saved.

Also: the name was parsed out of the history line with a pattern stopping at the first colon, so any product whose name contains one was truncated. The match is made on the untruncated remainder now.

v1.8.566

The downgrade fix, verified on the quote that found it

71635409 has two items quoted 9+ and graded 9−. One’s note reads “Missing Accessory: Canon USB Cable”, the other’s “Not the item quoted.” — both real notes, neither explaining a downgrade, and under the old rule the presence of any note at all was enough to skip the check. Both now raise the softer finding. The third item isn’t graded yet and is correctly left alone.

v1.8.564

The soft downgrade finding looked hard in the panel

Last release added a softer tier — “there is a note, but it never mentions the grade” — and left the quote panel’s list rows hardcoded red. So a row that only asks a question arrived looking exactly like a missing note, while its own inline block on the product was correctly amber. Two colours for one finding is worse than either. The row now follows the finding.

The header counted them together too, which overstated the hard ones and hid the soft: it reads “1 downgrade unexplained · 2 to check” now.

v1.8.563

The downgrade check had a hole, and 71635409 fell through it

A 9− item quoted 9+ that never flagged. The check skipped any product with any Inspection Note at all — and that item has one, about something else.

That looseness was deliberate and its reason still stands: the rule used to demand a note opening “Grade due to”, but the manual’s own approved wordings include “Dust levels found are too excessive for our Mint Grade.” and “Shutter count 250k – Exceeds limit for a grade 8”, so correctly-written items were listed as unexplained on every quote. Tightening back to one opening would just swap the hole for the noise.

So presence and relevance are now separate questions. No note at all is still the hard finding. A note that never mentions the grade — in any of the vocabularies those approved wordings use — is a softer one that asks rather than accuses: “there is a note, but it never mentions the grade. Does it explain the downgrade?” Judging prose still isn’t this tool’s job; noticing the prose never goes near the subject is.

v1.8.563

A stand-in serial, and serials on the quote overview

On an item with an empty serial field, a button offers quote reference dash a number — 71635409-1, then -2 — counting up across the quote so two unserialised items on one quote stay tellable apart. The number is allocated on the click, never on the render, so looking at five items doesn’t consume five numbers; re-open an item and it shows the number it already has. It never appears over a serial that is already there.

The quote overview now shows each item’s serial inline, and marks a line added after the quote was raised with a distinct EXTRA LINE pill in its own colour — not one of the three the accessory pills use, because it’s a fact about the line, not a state of the item. NOVA records nothing about when a line was added, so this reads the convention (those words in the notes) and says nothing when the convention wasn’t followed.

v1.8.563

A capture no longer dies because Sheets wasn’t signed in

The capture arrives, you press Fill, Google Sheets isn’t connected, it fails — and the payload only ever existed in the URL that carried it and the modal it opened. Closing that meant walking back to NOVA and saving the item again.

Two fixes. The sign-in is asked for before the write, not discovered by failing at it — through the same mechanism the rest of the app uses, which re-fires the save itself once the token lands. And the capture is kept until something has actually been done with it, so there is always a way back to it.

v1.8.562

Packaging gets ticked Received wherever it came from

It only ever ticked inside this tool’s own “no white box — what does it go in?” prompt. Add a brown box through NOVA’s own Add another, or let NOVA’s White Box Yes add one, and nothing ticked it — and an unticked row is exactly what the website shows the customer as Not Included. Your own rule was “autotick brown box or padded envelope as received if added”, which doesn’t care who added it.

It’s a standing rule now, run on every settled change rather than once at arm, because a row that arrives over the wire arrives long after the tool started watching. Once per row: untick it by hand and it stays unticked, the same way the battery-health line already works. If NOVA marked the row Replace itself, it’s left alone — ticking Received alongside it is the contradiction the save gate blocks.

v1.8.561

The resolve animation had never once played

A design check flagged the collapsing row for animating max-height. Looking at it turned up something worse: the rule transitioned max-height from its default of none to 0, and a transition out of none has no start value to interpolate from — so it never ran. The row snapped away and the 200ms wait behind it just delayed the rebuild. The strike-through you could see; the collapse you were meant to see after it, you never did.

It now collapses with grid-template-rows: 1fr → 0fr, which needs no made-up height to count down from and is one property instead of three.

v1.8.560

“Incorrect serial number listed on NOVA” was firing on every inspection

The scored-error check is generalised, not per-item — it matches a manager’s reason against what’s on the page. That reason was matching because its words serial and number both came from the field label “serial number”, which is in play the moment any serial is typed. Two hits, threshold met, and nothing about the item actually resembling that mistake.

Field names are now kept separate from page content, and a match needs at least one hit from something actually on the page. A label tells you which part of the form a reason is about, never whether this page has the problem — and since every scored reason names a field, without that rule every one of them fires on every item.

v1.8.560

A fresh item opened with a list of jobs that were already done

Two separate causes behind “a big list of to-checks before I’ve started, and they don’t update until I re-run it”.

Rows that could never change were in the work list. “Ticked shutter count not required”, “Set Sensor Quality to N/A”, and the notes typed on your behalf are reports of what the tool already did at arm. They are true when the page opens and still true at the save, so they sat there looking like outstanding work that never cleared. They now have their own tier — a collapsed Done for you group at the bottom, out of the count, with the faint mark still on the field itself.

And the watcher was narrower than the checks. It listened on #PageForm, but the accessory table, the packaging rows NOVA adds over the wire, and anything re-rendered outside that element sit outside it — and half these checks are about accessory rows. It now watches at document level, the way the phrase strip always has, plus a mutation observer on the accessory table so rows arriving over the wire re-check too.

v1.8.559

Two edge cases in yesterday’s work, one of them mine

The relay could pick a tab you cannot see. It addressed the hand-off to whichever app tab wrote the heartbeat last, which was enough to answer “is anything alive” and wrong for “where should this go”. With a forgotten app tab open in the background of another window, the hand-off could be addressed to it — the tick would still land, but a confirm would open somewhere off-screen and from your side nothing would have happened. A hidden tab now declines to overwrite a visible tab’s claim, and switching to a tab claims it immediately rather than up to four seconds later.

“+ kit” is inserted, then measured. Adding a button inside a table cell is a guess about someone else’s layout, under a theme that can restyle any of it. If it pushes the accessories table wider than its container it takes itself back out — the same discipline the navbar chip already follows. A convenience button is never worth a table that now scrolls sideways.

v1.8.558

Closing the panel no longer switches the tool off

You closed the box in the top right because it sits over the form, and the suggested phrases went with it. That was the visible half of a bigger problem: the x called teardown — the field markers, the phrase chips, the Save gate and every watcher on the page. “This is in the way” was being answered with “then you get none of it”.

The x now puts it away. The panel goes; everything else keeps running. In its place sits a small stub carrying the count — click it to bring the panel back. Escape still collapses to the header first, so there are two sizes of smaller before anything is given up. Shift-click the stub if you genuinely want the tool off this item.

Also fixed while in there: collapsing the panel by its own header rebuilt its class list from scratch and dropped the positioning class, so it jumped.

v1.8.558

“+ kit” on the Original Box row

A button beside the Original Box note that writes Kit into it. It appends rather than overwrites — anything already in that note is yours — and once the note says kit the button greys out and says so instead of offering again.

On the row rather than in the panel, for the same reason the phrase chips are: a button that writes into a field belongs beside the field, not in the box you keep closing.

v1.8.558

A shutter count that was actually read outranks the checkbox

The capture was telling this app whether NOVA asks for a shutter count — the “not required” checkbox, inverted. That is a request, not evidence. A figure actually sitting in the field is proof one can be got off that model, which is exactly what the gear DB’s Shutter Count Attainable column claims, and the two can disagree: the checkbox is a default, the number is a reading.

So the capture now also sends whether a count (and a firmware version) was really entered, and both the add form and the quick router prefer that over the checkbox — saying so when they do: “attainable — a count was read on this one”.

The boolean, never the number. A shutter count is a fact about the unit in front of you; the gear DB is a per-model record, so the figure has nowhere true to live in it. Sending it would be the first per-unit condition fact to cross a line the capture has held since it was written: product-level facts only, no serial, no customer block, no quote reference.

v1.8.557

A hand-off lands in the app tab that is already open

NOVA lives on the left portrait screen; this app is always open on the right. Every hand-off from the bookmarklet — a tick, a capture, a swapped item, a finish — opened a new tab, which the browser put in the NOVA window. On the wrong screen, stacking up all day, and for the ones that show a router or a confirm it put the thing to act on in a tab you weren’t looking at.

Positioning that new window was the obvious fix and is the wrong one. Placing a window on a named screen needs the window-management permission and a popup rather than a tab, and it would still open a second copy of an app that is already running.

So the arriving tab hands over and gets out of the way. It writes the query string to storage this origin shares across tabs, names the tab that should act on it, and closes itself. The tab that acts is the one on the right screen with today’s plan already rendered — no permission, no popup, no accumulation, and the confirm opens where you are looking.

Addressed to one tab by id rather than broadcast: with two app tabs open, a broadcast would put the same confirm in front of you twice. A tab announces itself every four seconds and counts as gone after three missed beats, so with no app tab open the hand-off simply opens one, exactly as before.

The spares label is deliberately not relayed. It ends in a print window, and a window opened from a storage event has no user activation behind it — relaying that one would trade a tab on the wrong screen for a blocked print. It keeps its own tab.

Re-copy the bookmarklet from Guide section 11: the hand-off tab can only close itself if a script opened it, so the tools dropped noopener on the app hand-off. Canon’s keeps it — that is a third party’s page and nothing there needs to close itself.

v1.8.557

The version number had been stuck at v1.8.549 for seven releases

v1.8.550 through v1.8.556 shipped without bumping index.html or the service worker, so the cache key never changed and the version chip kept saying 549. If the last week of NOVA work seemed to not be there, that is why. This release bumps both.

v1.8.556

Inspection Complete finishes the quote here — the last unmet line of TO ADD.txt

When NOVA has a quote at Inspection Complete, the panel offers to finish it in this app. It reads NOVA’s own status text, so it never guesses — it only appears for a quote NOVA itself calls complete.

A button, and a confirm, and neither is a formality. Finishing stamps a completion time that feeds today’s pacing and drift. And the two systems don’t mean the same thing by “done”: NOVA is complete when its inspections are saved; this app is complete when every line is ticked through inspect / nova / admin. Firing on arrival would let one system quietly overrule the other about your own day.

Where they disagree, it names what’s outstanding rather than counting it — “these aren’t ticked here: Canon EF 400mm, Canon Extender” rather than “2 lines open”, because a count sends you hunting. It finishes nothing in that case.

The quote is re-resolved against live state after you confirm, the same discipline as every other hand-off: a cloud merge can replace the object graph while a confirm modal is on screen, and mutating the old one would report success while changing nothing.

v1.8.555

Right-side pills, above Last saved

You were right on both counts. Last version rendered the inspection’s accessories as a text list, and put it below Last saved — both because the accessory state arrives from a fetch that finishes long after the history blocks have already been added, so it landed last by default.

The right column now shows the inspection’s own accessory list as pills: green received, blue replaced, red not received, headed with the count. It inserts before the history blocks, because what the item actually contains outranks when it was last touched.

Pills rather than a list because the left column is pills — they’re the same kind of fact, what came with this product, seen from two sources. Showing them in two different shapes invites reading them as two different kinds of thing.

Original Box and Packaging stay out of it: packaging has its own White Box flow, and including it would put a red pill on every item whose box was correctly marked Replace.

v1.8.554

Accessory pills show what actually turned up — and know when not to

The left column lists what the customer said was included. Whether each one arrived is only knowable from the inspection, which this tool already fetches for Spares — so the same read answers both.

Left: each pill goes green when the accessory was received or replaced, red when it wasn’t. Right: everything the inspection says didn’t arrive — including accessories the customer never mentioned, which by definition can’t appear on the left, and which still show to the next customer as Not Included.

Your swap caveat is the reason this isn’t just a diff of two lists, and it’s the thing that would have made it actively misleading. If the item on the bench isn’t the item quoted, the customer’s accessory list describes a different product — marking those pills missing would be confidently wrong about a lens that was never here. So when the inspection carries “Not the item quoted”, the left pills are left completely alone and only the right-hand list shows, saying why.

Matching is generous about finding a pair and conservative about concluding: brand names like “Canon” are ignored as matching everything and distinguishing nothing, and a pill with no match stays neutral rather than being called missing — “I couldn’t find this in the inspection” and “this didn’t arrive” are different statements, and only one of them is ours to make.

Checked end to end on your own six-item quote: 18 of 18 pills matched, no neutrals, no false reds. That quote is fully complete, so the red path is covered by tests rather than seen in the wild yet.

v1.8.553

How long each item actually took, from the History log

Each product now shows an approximate working time under Last saved, and the panel totals the quote. On your own six-item quote it read ~67 minutes across 6 items from 14 measured gaps.

What it measures, precisely, because this is easy to overclaim: the gap between one save and the previous save on that quote, attributed to the item just saved. That’s the working time leading up to a save — the closest thing the log can honestly offer.

What it isn’t: time spent inspecting. You might work an item for ten minutes and save once, or save four times while barely touching it. The first item on a quote has no previous save to measure from and shows nothing rather than a guess.

Gaps over 45 minutes are dropped, not counted — a three-hour gap is lunch or the next morning, and folding that in would make the whole number worthless. A number that’s sometimes nonsense is worse than no number. Where one was dropped the item says so.

It reads “~7 min”, not “7 min”, deliberately: the tilde invites about the right amount of trust. This deliberately does not write to your actuals — it’s an observation to look at, not a measurement good enough to overwrite what you recorded yourself.

v1.8.552

Four animations the theme could never do

Your Stylebot theme animates state beautifully — grade tiers, hovers, reveals. What CSS can never do is react to an event, because it can’t know a check just passed, a finding was resolved, or text landed somewhere. That’s the half the bookmarklet can see.

The field a phrase lands in flashes. The chip you click sits below the box the text appears in, and the overlay covers the page entirely — so until now nothing connected the click to the result. Answers “where did that go”.

A finding that’s new since the last check snaps its outline inwards. An outline says “wrong”; it can’t say “this just became wrong” — the difference between something you already knew about and something your last keystroke caused.

All clear gets one confirmation, and only on the transition — arriving at an already-clean item stays silent, because nothing was achieved by opening it. The list going from something to nothing is a real completion; progress toward one isn’t.

Spares blocks fade in as they arrive on the quote page. They land one at a time over a couple of seconds as each item is read, so without it they pop into a static page at random intervals and read as jank rather than progress.

All under 300ms, one per event, reduced-motion honoured. Two things went wrong building it, both worth recording: an edit script aborted after applying changes in memory but before writing, leaving flash() called twice and never defined — a runtime error node --check passes happily, since it only validates syntax. And the first version of the new-finding animation drove outline-color, which fights the tier colour the code sets inline; it animates the offset instead, which nothing else touches.

v1.8.551

Four theme corrections — including a field that never showed it was required

Category never showed a required marker. It carries data-val-required, but its ::after is the product-type icon, which replaces the asterisk rule rather than sitting beside it. So the one field you can’t save without has never been marked as required. The asterisk now rides the renamed label text instead.

Shutter Count showed a required asterisk while “not required” was ticked. Two contradictory signals on one field, re-read every item. The asterisk rule keys on a static attribute that never changes, so nothing cancelled it. It’s cancelled now while the box is ticked, and returns the moment it’s unticked.

The checkbox overflow, at its real cause. Not the font-size:0 label — the wrapper still carries Bootstrap’s col-sm-4 percentage cap, which in an auto-sized grid track resolves to 38px, leaving a 30px label for a 45px box. The same cap was lifted for the input wrappers long ago; these two never got the line. Verified live: wrapper 38→113px, and the box now sits inside its label.

The Save/Next/Back dock answers to the keyboard. It revealed on hover only, so Tab could land on a control still 20px off the bottom of the screen — focused, invisible, about to be pressed.

Two notes worth keeping. Testing theme overrides by injecting CSS into the page doesn’t work — Stylebot wins over the page regardless of specificity, so the test silently does nothing and looks like a broken selector. And the dock reveal uses bottom:0, not a tidier 24px: your own file records that a gap there causes a jitter loop, and 24px was my first instinct. The reveal value isn’t a free choice.

v1.8.550

The NOVA layout idea was tested and rejected — with the numbers written down

A review made a confident case: your pages look cramped because 1080px is below Bootstrap’s 1200px breakpoint, so NOVA’s two columns stack instead of sitting side by side. Force them side by side and the item page should fit one screen.

The first half is exactly right and measurable. The fix makes it worse. Tested at four ratios on a real item:

LayoutForm colAccessories colPage height
Stacked (today)976px976px2293px
58/42569px407px3728px
70/30683px293px3752px
75/25732px244px3800px

The accessories table can’t live in a narrow column — Name, Note, Received, Replace and the delete button don’t compress, so it blows up and drags the row to ~3300px. NOVA’s stacked fallback is genuinely the better fit at your width.

What did help, and is now in your theme: letting the auto-generated boxes take the height their text actually needs instead of a fixed 134px and 86px. The accessories move up 50px, the page loses 50px, and every character stays visible. Modest and honest — and unlike collapsing them behind a click, nothing is hidden and no keyboard path is lost, which matters because Preset Description is disabled and a disabled box can’t be focused at all.

All of that is written into the theme with the measurements, in the style the file already uses for reverted experiments — so the next person to have this idea finds the numbers instead of repeating the afternoon.

v1.8.549

The spares window waits behind NOVA’s labels, and a theme rule that was lying to you

Print the spares after the main labels. The ordering falls out of when things happen rather than needing to be scheduled: the listener runs on the capture phase, so our tab opens before the form submits, and NOVA’s PDF then opens on top of it. Focus is handed straight back to the quote page too. You print NOVA’s labels, close that tab, and the spares prompt is sitting there waiting.

It gets its own window rather than reusing the one the save hand-off uses — landing a print prompt in that tab would replace whatever was in it, including a capture you hadn’t dealt with yet. A blocked pop-up now says so on the page instead of leaving you waiting for a window that was never allowed to exist.

A leftover theme rule removed. It turned the customer-facing Inspection Notes box light grey with a red border the moment the form validated — a correct field styled as an error, and near-unreadable with the dark theme’s text colour. Pre-rewrite leftover; nothing replaces it, because a valid field needs no styling.

And the Sensor Specifics hide is confirmed correct, not changed. A review flagged it as the riskiest rule in the theme — hiding an editable field on an assumption the file itself called an open question. The assumption was right: it’s hidden until Sensor Quality says marks (effect), which is exactly when it’s needed. Now recorded as answered, so it stops costing a second look.

v1.8.548

Why the spares label can’t go inside NOVA’s own label — measured, not assumed

The idea was sound and it doesn’t work. Open a named window ourselves, retarget NOVA’s print form into it, and append our spares label to the document that comes back — same origin, same POST, nothing asked of their server. That endpoint returns a PDF. Verified with one request read for its headers only: %PDF magic, 28,518 bytes, application/pdf. A PDF goes to a plugin viewer; there is no document to append to. Not fragile — impossible.

It cost one request and rendered nothing, so no print dialog. That’s written into the source now with the numbers, so nobody spends an afternoon rediscovering it.

What’s left, if the two labels should ever share a sheet, is physical rather than clever: match this app’s label page size to the one inside that PDF and print ours onto a spare label on the same stock.

v1.8.547

The spares offer now fires on the button you actually press

There are two kinds of print button on a quote page, and your own theme already knew — it targets #btnPrintProductLabel.btn-primary separately from the rest. Six per-product buttons each carry a productNumber of “N of 6”; the one in the bottom-right dock carries no product number at all, so the server returns the whole set. That’s the one you use, and the spares offer was never firing on it — “Print All Product Labels” doesn’t contain “print product label” as a contiguous phrase.

Both are handled now. Printing one product’s label offers that product’s spares; printing the whole set offers once for the quote, since which product each belongs to stops mattering when every label comes out together.

They’re told apart by whether the form carries a product number, not by their words — because the words are what your theme rewrites. It relabels the dock button via ::after, so matching on text would mean reading your own styling back as if it were NOVA’s.

v1.8.546

The spares label follows the item, not the quote

Yesterday’s version fired once per quote, on the reasoning that a spares label goes on the bag. Looking at the page properly says otherwise: each product has its own Spares box, and NOVA prints each product’s label as a separate form POST carrying its own “N of 6”. So the product whose Spares box has content is the product whose label needs a companion.

The old version was wrong in both directions — it offered a label after printing a product that had no spares, and offered nothing for the second product that did. Now it’s keyed by each product’s own inspection link, which is the only stable identifier on a page where all six print buttons share a single id.

On adding it to NOVA’s own label sheet: there isn’t a sheet. There’s no “print all” — six separate buttons, each POSTing to /Quotes/PrintProductLabel and getting one server-rendered label back. There’s no list to append a row to, so ours prints alongside.

v1.8.545

A spares label offered when you print product labels

The quote tool already reads every item’s Spares box, so it’s the only thing that knows whether a quote has spares at all — NOVA’s quote page doesn’t carry the field. Now, when you press Print Product Label on a quote that has some, it opens this app with the spares label ready.

Once per quote, not once per product. Printing six product labels shouldn’t queue six spares labels — the spares label goes on the bag, not the item.

The label itself stays here rather than being rebuilt in the bookmarklet, which would be two designs to keep in step. The app asks before printing, and that isn’t politeness: a tab that has just loaded from a URL has no user activation, so opening the print popup straight away would be blocked outright. One click makes it reliable, which beats a silent failure.

v1.8.544

“12 mins ago” under Last saved

A clock time answers when; the question you’re actually asking is whether an item is fresh or whether you did it before lunch — and a timestamp makes you work that out yourself. Each product’s Last saved now carries the elapsed time under it, and so does a grade changed by someone else, where it matters more.

This does parse NOVA’s timestamps, which the ordering deliberately doesn’t: the order was already given correctly and re-deriving it could only add error, whereas elapsed time can’t be had any other way. Anything unparseable simply shows nothing rather than a guess, and a save a minute in the “future” from clock skew reads as “just now” instead of something daft.

v1.8.543

Guide section 11 rewritten — it was describing this morning’s tool

The bookmarklet documentation had fallen a long way behind. It still said the capture was “not automatic: it would overwrite whatever else you’d copied” (automatic since v1.8.510, and it hasn’t touched the clipboard since v1.8.539), still described the quote check looking for a “Grade due to” note, and mentioned none of the phrases, the typing, Spares, the History reads, the serial marks, the staleness stamp, the arrivals feed or the scored-error checks.

It’s now organised by where you are — on an item, on a quote — rather than by which tool does what, which is how you actually meet it. Buttons are split into the ones always there and the ones that appear only when they apply, so the panel stops looking like it changes at random.

“What it never does” is split in two as well: judgements it won’t make, and data it won’t move. Both matter and they were tangled together.

The step banner on the Today tab is updated to match, and now says plainly why re-copying matters: your gear DB, phrases, rules and error list are all baked into the copy.

v1.8.542

Your own scored mistakes, turned into checks

Your idea, and the best one available. The tracker’s Feedback tab is a real log of errors a manager has scored against you, with the reason and the specifics — ground truth about what you actually get wrong, which beats anyone guessing which checks matter.

Those reasons are grouped, counted and baked into the bookmarklet. When an item looks like the ground you’ve lost marks on before, the panel says so: “You have been scored on this 4 times: Grade too high for the dust level — last time: Graded 9+ with heavy dust in the optics”. A mistake made six times outranks one made once, and the most recent example comes along because “accessory not marked as received” is a category while “LP-E6N left unticked” is the thing you’d recognise.

Always a check, never a save-blocker, and never claiming you’ve made the error — it can’t know that. Only that this looks like familiar ground, and how often. At most two at a time: a nudge, not a rap sheet.

Only reason, specifics, count and item name are carried — no quote numbers, no dates, no manager names. The quote number is the one field that ties a mistake to a customer’s job, and the check doesn’t need it. It goes into a page that only ever displays it.

Getting the matching honest took two goes. It first matched the example text as well as the reason, which looked like more evidence and was the opposite: an example reads “LP-E6N battery left unticked on a 5D Mark IV”, so its product name matched the product on the page and every 5D scored three hits before anything was typed. It fired on a blank form in testing. Reason-only now — plus the names of fields that have something in them, since a manager’s reason talks about the form (“grade too high for the dust level”) and matching values alone could never reach those words.

v1.8.541

A feed of what NOVA has handed over, and what became of it

Every hand-off — product facts, nova ticks, changed items, battery markings — was applied and then forgotten. The capture router is a modal the next save navigates away from, and the ticks only ever produced a toast that’s gone in seconds. So “did that save actually land in the app?” had no answer, which is exactly the question worth asking after a day of them.

Guide → NOVA tools → Arrived from NOVA now lists them, newest first, with the time, the kind, and — the column that matters — what the app actually did. Not “product facts arrived” but “quote 71637779 isn’t in today’s plan”, “already ticked, no change”, “marked as having no health reading, covers 4 models”.

Captures are logged on arrival rather than on what you do with the router, because “it arrived and I never dealt with it” is precisely the state worth being able to see.

Kept on this device in its own store, capped at 60. It deliberately doesn’t live in settings — an arrival log is a fact about this browser, and putting it there would push a cloud write on every single save.

v1.8.540

Inspection Process Manual v1.1 reconciled — including one rule that was plainly wrong

“3rd Party”, not “3rd Party (Unbranded)”. The manual spells the wording out and lists the variations it doesn’t want — THIRD PARTY, Third Party, 3RD PARTY, Third-Party. “(Unbranded)” appears nowhere in the document. This was an exact-string check firing as a save-blocking fix, so it had been demanding a suffix the manual never asked for and rejecting correctly-written caps.

The Figure 8 process was wrong too. It isn’t a comment, it’s a cross-reference: “See Spares” in the charger comments, then “Figure 8 Lead (Replace)” listed in the Spares box — or under #NOTINCLUDED# in-store where it can’t be replaced. The old wording (“Missing Figure 8 Lead”) isn’t in the document at all.

99 approved wordings added to your phrase library. Whole families it had none of: viewfinder moisture and debris, delamination and clouding, serial-number notes, ports and doors, mounts, corrosion, tripods, buttons and dials, the battery-health sentences for Canon/Nikon/Fujifilm/GoPro, the customer-friendly decline reasons. Added by label, so anything you’ve reworded keeps your wording and nothing you deleted comes back.

New grade ceilings for the three metering faults, and the “below two bars” battery decline the reissue adds.

And the two you reported live. A lens was being offered battery phrasing — the demote list had “battery compartment” but not “battery”. Lenses and converters now have their own rule, because a flashgun is also “not a camera” and genuinely does have one. Battery-health wording is now restricted to camera bodies and battery lines specifically, so a tripod keeps “Mathorn” but loses recharge-performance sentences. And “Grade due to dust in the optics” is in, in both forms — the most common lens downgrade there is, and the manual gives the moisture and debris versions but not the plain dust one.

v1.8.539

Category-aware suggestions, and seven things a second review pass found

Suggestions now know what kind of item you’re on. Your question, and the honest answer was “effectively no” — it read the Category, but “camera” and “lens” are both in the manual’s stop-word list, so it contributed nothing. A lens no longer gets offered sensor and shutter phrases; a body no longer gets zoom-ring and front-optic ones. It’s a demotion, not a filter — type toward one and it’s still there, and the full library is unchanged.

The phrase cap was dropping the manual’s own wordings first. It trimmed newest-first on the reasoning that a phrase written today matters most — but the seeded defaults sit at the front, so it dropped the grey-import sentence, the third-party battery sentences, the missing-battery-door note, the mite and delamination notes: exactly the ones the other checks are about. Silently, while the toast still counted them. Nothing is anywhere near a size limit, so the ceiling is far higher now, required wordings are pinned and can never be trimmed, and if anything is left out the toast says how many.

“This battery has no health reading” was a dead end. It stopped future typing but left the X/4 already in the comment — so the save-blocking fix stayed and you still had to delete it by hand, after pressing the button whose whole job was to stop being asked. It now removes the figure, and only if the text is still exactly what the tool typed.

The automatic capture no longer touches your clipboard at all. Last version silenced the failure but left the worse half: on arms where it succeeded it replaced whatever you were carrying, with the toast that used to say so now removed. Nothing needs it — the save hand-off reads the payload directly — so the button is the only thing that writes the clipboard, which is what the Guide always claimed.

Three more: the floating panel was opening collapsed (it decided from “a navbar exists” rather than “the chip actually mounted”, and the chip removes itself on this page); the blocked-pop-up warning was being consumed by the first re-check, so it existed for one render and then animated away as if fixed; and arming a quote page twice could append the Spares block twice for any item whose first fetch hadn’t landed.

And a hardening: the Spares read now takes the View Inspection link by id rather than the first inspection link in the block. On an in-progress quote that’s the only one there — but NOVA also has a “start inspection” link, and fetching that would mean this tool starting inspections as a side effect of reading a field.

v1.8.538

The History tab, read and put on the products — and the quoted condition follows you into the item

NOVA renders both quote tabs and hides one, so the History log was already sitting in the page costing nothing to read. It carries who did what, when, and a field-level diff. All of that was one tab away and then a scroll through twenty-odd entries.

Each product now shows when it was last saved and by whom (“you” when it was you), and an item with no history at all is flagged Never saved — no inspection has ever been written against it. On a quote you’re about to mark Inspection Complete that’s the single most useful thing on the page and the hardest to see. Caught a real one on the first quote it ran against.

A grade changed by somebody other than you gets called out with the name and time. Your errors are scored, so a grade you didn’t set is worth seeing without going looking. It works out who you are from NOVA’s own account dropdown rather than anything hardcoded — scoped to the navbar, because the customer’s email is on that page too and confusing the two would label your own work as someone else’s.

The panel gained a one-line roll-up: how many are never saved, how many were graded by someone else, or “All 6 items saved”.

And the quoted condition now follows you into the item. The item page has never known what the customer was told — only the quote page does — so the downgrade check could only ever catch things afterwards. The quote tool parks it, and the pre-flight reads it back, so “this needs a sentence” is asked while the notes field is still in front of you. Absent unless you opened the quote first, and absent is silence rather than a guess.

v1.8.537

The suggestions now know which shared words actually mean something

The first version scored one point per matching word. So “light” — which appears in Light dust, Light peeling, Light delamination and a dozen more — lifted all of them equally, while delamination, which appears in one phrase and settles the question outright, scored exactly the same. Rarity is the entire signal and it was being thrown away.

Each shared word is now weighted by how rare it is across your library, and the stop-word list and synonym map already transcribed from the manual are finally being used — they were written for the grade-cap matcher and the ranking had been ignoring them. So “rear screen” now finds the phrase that says “rear LCD”, and “light” on its own correctly suggests nothing at all.

And your dropdowns are treated as answers, not words. Dust Level and Sensor Quality are enums you’ve already chosen from, and each choice has a sentence that goes with it. The phrase matching your selection is pinned first, outlined in lime, and its tooltip says which selection it came from. It has to match every distinguishing word of what you picked — a pin claims “this is the sentence for your choice”, and that claim has to be earned. Still offered, never typed: a lens with dust in the viewfinder needs a different sentence from one with dust in the optics, and only you know which you saw.

One bug found in testing this, in the fix itself: the synonym map deliberately shortens the most distinguishing words — viewfinder becomes vf, autofocus becomes af — and a three-character minimum was throwing exactly those away. “Dust in the viewfinder” was tying with two optics phrases as a result.

v1.8.536

The suggestions narrow as you type, in whichever box you’re in

Type dust and the chips narrow to the dust phrases; zoom leaves one. It filters on the line you’re currently writing — not the whole box, because a notes field accumulates sentences and filtering on everything already there would only ever narrow to what you’ve already said.

The strip also follows you. It attaches to whichever notes box has focus — Inspection Notes or Unique Product Notes — and clicking a chip inserts into that box, so the fault you write internally and the sentence the customer reads both get the same library. With nothing typed it falls back to the old behaviour: phrases matching what the page already says.

Two details that only show up in use. Chips insert on mouse-down, because the field loses focus the moment the button goes down and a focus change would otherwise rebuild the strip out from under the click. And it runs on its own short timer rather than the checks’ 300ms pass — rebuilding one small element should feel immediate, re-running every check on each keystroke should not.

v1.8.535

Suggested phrases sit under the notes field, not behind a button

The phrases this page’s own wording says you probably want now render as a row of chips directly under the Inspection Notes box — where you’re typing — instead of inside an overlay you have to open, read and dismiss. Click one to add it. One already in the notes shows struck through in green rather than disappearing, so the row doesn’t reshuffle under you.

Up to six, and only ones that actually match; if nothing matches, nothing appears. The panel button becomes the way to the whole library and says how many are already suggested below, which is the useful thing to know when deciding whether to open the full list at all.

They’re injected as plain spans, never form controls. NOVA serialises the form when you save and only named inputs, selects and textareas go with it — a span can’t be submitted, so this can sit inside the form without becoming part of what gets written. Verified on a live page: zero form controls added.

v1.8.534

Everything says it on the item, not in a message that fades

Three things moved out of toasts and panels and onto the thing they’re about.

Spares now load automatically and sit on each product. They were behind a button and listed in the panel; they read every item on arrival now, and each one’s Spares box appears inside that product’s own column, under its accessories. Still sequential — it’s one request per product against NOVA’s server — so on a six-item quote they arrive over about two seconds, item by item.

Unexplained downgrades are marked on the product too, on the inspection side next to the Grade and Notes they’re about. The panel stays as the count and the jump-list, for “is there anything here at all”; the detail lives with the item.

The serial check stops relying on you catching a toast. Once Canon’s tab is open, the step-2 instruction sits beside the serial field until you deal with it, and a Mark serial checked button appears while a check is outstanding. Marked serials keep a quiet note on the field reading “canon checker run for this serial”.

It says checker run, not verified, deliberately: the tab that knows the answer is canon.co.uk, a different origin that can’t report back. The only honest claim this side can make is that the check was made. It’s keyed by serial rather than by product, so a different unit of the same model correctly shows nothing, and it lasts the session — NOVA’s longer-term storage gets wiped, so a bigger promise would be one it couldn’t keep.

A blocked Canon pop-up now says so on the item as well, instead of failing quietly.

v1.8.533

Phrases where you actually type them, and five fixes from watching a real save

Your Reference Phrases now live in NOVA too. They were only ever in this app, which meant the moment you needed one you were in the other window. The pre-flight panel has them now, and the button is context-aware in the only sense worth the name: it reads “4 phrases match this item” when they do, ranked against the fault wording, dust and sensor selections already on the page. Everything still appears — a rank is a guess, and hiding a phrase behind a wrong guess is worse than a scroll — the guess only sets the order.

Ticking Declined with empty notes turns it into a primary button reading “Declined — write the reason”, pre-filtered. Inserting is add-only, never duplicates a line already there, and says so in the panel.

The clipboard box stops jumping in front of the panel. The capture runs automatically on arm, and the clipboard routinely refuses there because the page isn’t focused — which threw a panel of raw JSON over the pre-flight, for a copy you never asked for. A refusal is now silent when the copy was automatic. Click “Copy product facts” and you still get the paste-it-yourself fallback, because there the copy is the point.

A blocked pop-up no longer loses the hand-off in silence. A blocked window.open returns null rather than throwing, so the old catch never ran and the product facts simply vanished. It’s checked now — and since the page is already navigating away, it’s recorded and reported by the next arm, which is the first moment there’s anyone to tell.

The quote check stops crying wolf. It only accepted “Grade due to” as an explanation, but the manual’s own approved notes include “Dust levels found are too excessive for our Mint Grade.” and “Shutter count 250k – Exceeds limit”. Correctly-written items were being listed as unexplained on every quote. Any note counts now — the finding is a downgrade with no explanation, and judging whether prose explains the right thing isn’t something a word-match can do.

Two contradictory save-blockers on batteries, resolved. A battery comment mentioning third parties was told it must be exactly “3rd Party (Unbranded)”, then once it was, told that wording is for caps only — two fixes pointing opposite ways with no way through. The exact-wording rule is a cap rule and now only applies to caps; a battery or charger gets told what it actually needs instead.

And the copy button reports the size: the bookmarklet carries code, rules, gear facts and now phrases, all in a bookmark URL, so that number is worth being able to watch.

v1.8.532

Accessories on the quote page, one pill each

NOVA renders a product’s accessories as a single comma-separated run of bold text — “Canon LC-E5 Battery Charger, Canon LP-E5 Battery, Canon RF-3 Body Cap, Canon EF Eyecup, Manual, Canon Strap” — and at six or seven items you end up parsing the commas yourself, which is exactly when you miss one. Each is now its own bordered pill, wrapping onto as many lines as it needs.

This is the one place the quote tool touches NOVA’s own markup, because CSS can’t help here — there is nothing per-accessory to select until something splits the text. It’s presentational only, on a part of the page with no form behind it, so nothing about it can be saved, and a reload puts it back.

Pills are built as elements with their text set as text, never as markup — an accessory name is page content, and page content doesn’t get to be HTML.

Also: the Spares list added last version had a coloured bar down the side of every entry. Every entry there is the same kind of thing, so a colour on each one encoded nothing — they’re separated by a rule now. The red bar on the findings list stays, because there it marks a finding.

v1.8.531

Four bugs found by testing the tools on a real NOVA page, and the Spares boxes on the quote page

The note beside a “not required” box wrecked the row. Your report, confirmed live and measured. The theme hides those two labels’ text with font-size:0 to show the X icon — but that text was the only thing holding the column open, so the label collapses to 30px and the 45px box already overflows it by 12px. Handing that label an inline note grew it to 148px and dragged the whole Shutter/Firmware row 422px to the left, shrinking the Shutter Count field to a third of its width, with the note itself rendering one word per line. Notes now measure their host and skip anything too narrow to hold a sentence — the field still gets its outline and the panel still carries the words, so nothing is lost.

The panel’s buttons were the wrong size, invisibly. font:600 13px/1 inherit is invalid CSS — inherit can’t be the family inside the shorthand — so browsers dropped the whole line and the buttons rendered at 16px regular instead of 13px semibold. That’s why “Copy product facts” wrapped onto two lines. Split into longhands.

The navbar chip has never once been visible. It mounts into NOVA’s navbar, but that row’s third child already fills its entire width, so the chip landed at x=1317 in a 1080px window with no scroll reaching it. Worse than losing the chip: the panel deliberately drops its own count when the chip exists, so the count was showing nowhere, and the chip is also what you click to collapse the panel. It now checks where it landed and falls back to the floating panel if it can’t be seen.

Spares no longer blocks a save. Your correction: the Spares box on a quote’s top line carries the quote’s actual spares, which need no “SEE SPARES” against a box row — that link is for a bonus extra bundled with this product. The page can’t tell the two apart, so it now asks rather than blocks.

And you can see those Spares from the quote page. NOVA doesn’t put the field there at all, so the quote panel gained a Show the Spares boxes button that reads each item and lists whatever is in them. Behind a button, one item at a time, because that’s a real request to their server per product — measured at about a fifth of a second each.

v1.8.530

The typing happens when it should, and stops happening when it shouldn’t — plus the bookmark can now tell you it’s old

“Missing Battery Door” could be typed into your customer-facing notes about a door that was fine. The rule was “a Battery Door row exists and neither box is ticked”, checked once when you arm — but at that moment nothing is ticked. Unticked at arm means “not looked at yet”, not “missing”. So on any item whose accessory list already carried a Battery Door row, arming appended that sentence before you’d touched the camera, and being add-only it never came back out.

It now waits for the row to appear. The manual’s own process is to add the accessory when the door is missing, so adding it is the deliberate act the page can honestly read. A row that was already there and left unticked still gets flagged at save — where unticked genuinely does mean not received — it just doesn’t get written for.

The other half of the same bug: the typing mostly never happened at all. Both writers ran once, at arm. But the battery gets ticked Received and the door row gets added after that, in the normal order of working — so in practice neither fired and you typed by hand, while the Guide promised otherwise. They now run on a settled change too. The “not required” ticks deliberately do not: re-running those would re-tick a box you had just unticked on purpose, and a judgement of yours outranks a fact from the DB.

Each is written at most once per item, and never while your cursor is in the field it would write to — add-only writing plus a re-run is exactly how a sentence you deleted gets re-appended with the caret jumping to the end.

The bookmark can now tell you it’s stale. It has always been a frozen snapshot — the code and your gear-DB facts, baked in when you copy it, never updating themselves — and nothing anywhere compared it to anything. A bookmark from three weeks ago looked exactly like a current one until something went wrong.

The panel header now carries the date it was copied and how many gear facts it holds, and says NO GEAR DATA outright for a copy made before the camera DB had loaded — which silently has every gear-driven check switched off. The stamp also rides back on the save hand-off, so the app can compare it against what it holds now and say so, once, quietly, and only when something has actually moved.

v1.8.529

“Tick nova in the app” ticks the item you did, not the whole quote

Pressed from an inspection page, that button sent only the quote reference — so the app ticked every non-missing line on the quote. Finishing item one of five ticked all five, and awarded XP for four nova phases that hadn’t happened. Found in a fresh-eyes review of the bookmarklet, not in use, which is the uncomfortable part: it looked like it was working.

It now names the line as well as the quote, using the product name and Parent SKU it was already reading for the other buttons. If it can’t match that line in today’s plan it says so and ticks nothing — ticking everything because one line couldn’t be found is the bug, not the fallback.

A bare ?nova-tick=QREF still means the whole quote, deliberately: an already-installed bookmark sends exactly that, and it has to keep working. The line identity narrows; its absence is not a request to tick everything.

One more found while proving it: a quote carrying the same product twice could only ever have its first copy ticked. The second press re-found the first line, saw it already done, and said “already ticked” while the other sat there untouched. It now prefers a line that isn’t ticked yet.

Re-copy the bookmarklet from Guide section 11 — the old one keeps ticking whole quotes.

v1.8.528

The Original Box stops nagging once you’ve answered it, and a way to say a battery has no health

Original Box stayed highlighted after you added a Brown Box. The check fired on the Replace tick alone, so once you’d answered the White Box popup — through this tool’s own prompt or NOVA’s Yes — the row was still outlined and the panel still telling you to answer it, pointing at the one thing you had just dealt with. It now looks for the answer: a White Box, Brown Box or Padded Envelope row on the accessory list settles it.

The completion of that check comes with it: if the packaging row is there but not ticked Received, it says so. An unticked row is what the website shows the customer as Not Included, so that reads as “no packaging” on an item that has some.

“This battery has no health reading” is now a button in the pre-flight panel, and it only appears while the panel is actually asking you for a figure — an answer to what’s on screen, not a setting to go and find. Adding LP-E5 last time meant editing the rules by hand; this is the version of that you can do yourself, at the moment it annoys you.

It records the battery, not the camera. An LP-E5 has no battery-info screen whichever body it is in, so marking it once covers every model that takes one — the toast says how many, because that is wider than the button’s wording suggests. NOVA can’t do this itself: its accessory row is called just “Battery”, so which battery a model takes is a fact only this side holds, and the app resolves the SKU when the message arrives.

What you’ve marked lives in Guide → Batteries & chargers, with how many models each one covers and a Remove for anything marked in error. And because the panel can’t see the app’s answer until the bookmark is next copied, pressing the button also silences that finding for the rest of the item — otherwise it would look broken.

Re-copy the bookmarklet from Guide section 11 — the list rides in the copy, like every other gear-DB fact.

v1.8.527

Battery health: the figure alone, on the right row, and only where there is one

A charger was being read as a battery. “LC-E5 Battery Charger” contains the word Battery, and the test for a battery only looked for that — so the health figure was typed into the charger’s comments, a customer-facing field, on a part that has no health to report. The check that reminds you about a missing reading had its own guard against chargers; the thing doing the writing did not, which was the wrong way round.

It types the figure, not the sentence. Your call, and the field agrees with you: the comment is a narrow box read beside the part, and X/4 says what the twelve words around it said. Still X/4 for Nikon, X/5 for Fujifilm, X/X for Canon, Sony, Olympus, Panasonic, Leica and Pentax — the denominator is the only reason there is more than one.

Some batteries have no health to give. An LP-E5 predates Canon’s battery-info screen entirely, so there is nothing to put in the box — and NOVA’s accessory row is called just “Battery”, so the tools could never have worked that out from the page. The gear DB knows which battery each model takes, so the app resolves it at copy time and the bookmarklet simply gets a list: on those models it neither pre-types a figure nor asks you for one. Add more as they turn up — the list is one pattern in the process rules.

Two smaller things while in there. A missing reading no longer reports itself twice, once as a save-blocker and again as a reminder about the same row. And the save-blocker’s test for an unreplaced X now requires it to stand alone: written loosely, it matched the x inside an ordinary word before a slash, and a gate that fires on a word is how a gate gets ignored.

Re-copy the bookmarklet from Guide section 11 — the LP-E5 list rides in the copy.

v1.8.526

The Nova auto-tick actually works now

The bridge extension had three silent faults, and between them it was far less reliable than it looked. It ticked by clicking checkboxes in the app page — but those only exist while the Today tab is showing, so on any other tab the whole thing quietly did nothing. It ticked every item on the quote, not the one you saved, so finishing item one of five ticked all five. And it only recognised one save button: it watched for the redirect to the quote page, so every save made with “Save Changes / Next” reported nothing at all.

The extension is now a messenger rather than a doer. It notes which product is on the page when you submit, pairs that with wherever the save lands, and hands it over; the app ticks the item through the same code the bookmarklet route uses — so it works on any tab, waits properly for sync, and ticks exactly one line. If it can’t match what you saved to a line in the plan it says so instead of ticking everything.

It also queues. Previously an event went to whatever app tabs were open and was dropped if none were, so a save made before you’d opened the app was simply lost. Now it waits and is delivered when you open it — and an event that has been waiting since yesterday is discarded rather than applied to today.

The extension needs reloading in Chrome for this (chrome://extensions, reload), and it now asks for storage alongside tabs. Its description says plainly what it reads: quote reference, product name and Parent SKU — no serial, notes, grades or customer details.

v1.8.525

The battery health sentence types itself, leaving you the reading

A battery that is present and not being replaced gets its health sentence typed into the accessory comments, with the figure left as X for you: “The battery age is X/4, indicating full battery life remaining” for Nikon, X/5 for Fujifilm, and “The battery has X/X recharge performance remaining” for Canon, Sony, Olympus, Panasonic, Leica and Pentax.

Accessory comments rather than Unique Product Notes, on your call — and it turns out to be the right one twice over: that field carries data-forcelowercase, so “Mathorn” would land there as “mathorn”, and the tools have always looked for the health figure on the accessory row anyway.

It only ever writes into an empty comment, so it can never overwrite “Mathorn” or a reading you have already taken, and only for a battery that is actually there — a replacement carries no reading off the customer’s unit, and a missing one has no health at all. An unrecognised brand gets nothing rather than a guess.

And the necessary other half: a leftover X now stops the save. Accessory comments show on the website, so an unreplaced placeholder is customer-facing — that is a fix, not a reminder.

v1.8.524

Six real bugs from a review pass, and packaging gets ticked Received

Grade ceilings were matched across a whole field, not per sentence. “Light corrosion to the battery terminals. Heavy wear to the base plate” contains both “heavy” and “corrosion”, so it read as Heavy Corrosion — which the manual declines — and blocked the save. Matching is now per sentence, negations are respected (“No fungus, mite or moisture” is not three faults), and the words “no” and “not” are no longer stripped out of the rules, which had turned “rotates smoothly with no click feedback” into something a perfectly good dial matched. Decline findings are now checks rather than save-blockers: matched from free text, they can raise a suspicion, not settle one.

Sensor Quality N/A was selecting a disabled option. NOVA ships that option disabled on a camera page; selecting it looks like it worked, but the form then refuses to save with “Field is required”, and a disabled option isn’t submitted at all. It now skips any option NOVA has disabled.

The grey-import line could gate a save forever. The check compared a raw en dash while everything that writes the line flattens dashes — and NOVA normalises the dash when it saves. So a reopened grey-import item failed on every render, blocked every save, and couldn’t be cleared, because the add-only writer could see the line was already there.

“Save anyway” switched the tools off. If NOVA then rejected its own validation, the page was left silently unarmed with nothing to show it.

A Battery Door is not a battery. “Battery Door”, “Battery Grip” and battery compartment covers all contain “batter”, so a door drew battery-health reminders, third-party-battery notes, and at grade 10 a save-blocking “replacement battery caps at 9+”.

Dust levels let the worse ones through. An exact match on “heavy dust and debris” missed “very heavy dust and debris” and its three “affects f8>” variants — so the milder one was caught at grade 10 and the worse ones passed.

And your packaging rule: Brown Box or Padded Envelope is now ticked Received when it’s added, and nothing is ever written into the Original Box comments — that fallback is gone. If the add doesn’t take, the panel says so instead of typing the decision somewhere it doesn’t belong. It leaves the row alone if NOVA already marked it Replaced, since ticking both is a fault this same tool flags.

v1.8.523

Ticking Replace on a battery now names the approved replacement

You hit this live: ticking Replace on a Nikon EN-EL15 got “no brand named”, when the app has known since the day that table was written that an EN-EL15a/b/c takes a Mathorn EL15c. Two things were wrong. The message led with the third-party case — the rarer one — when ticking Replace usually means we are supplying the equivalent. And it couldn’t name the part anyway: NOVA’s accessory row is called just “Battery”, with no model on it, so the row name alone was never going to answer it.

The gear DB knows this model’s battery and charger, and the app already has the matcher. So the approved replacement is now worked out when you copy the bookmarklet and carried with it — no table and no matching logic duplicated into the tools, which just read the answer. Ticking Replace on that Nikon now says the approved replacement is Mathorn EL15c Battery, and that the comment reads “Mathorn”.

Where the table says a part has no approved third-party equivalent — LP-E17, MH-32, LC-E12E, DMW-BTC12 — it says that instead, and that comments may be left blank for a genuine one. Where the gear DB has no battery recorded for the model, it falls back to naming both approved brands rather than pretending to know.

Checked against the real parts: every EN-EL15 variant, MH-25a, EN-EL14a, EN-EL25, LP-E6/E6NH, LC-E6E, NP-FZ100, BC-QZ1, NP-W235, BLS-50 and NP-FW50 all resolve to the right row, and the four genuine-only parts are reported as such.

v1.8.522

A reworded phrase now replaces the old one instead of joining it

When the reconcile tool reported that the manual words an existing Reference Phrase differently, applying it added a second phrase under the same label and left the old wording sitting there — two versions of the same customer-facing sentence, with nothing to say which was current. These get copied straight into inspection notes, so that mattered.

A reworded phrase now replaces the one it names. Before you click, the row shows the wording you currently have against the wording the manual states, because overwriting customer-facing text without seeing what you are overwriting isn’t a decision you can actually make. If what it proposes already matches, it says so and offers no button.

It also cleans up after the old behaviour: if you already have several phrases sharing one label, applying collapses them to a single entry and tells you how many it removed. A label is a phrase’s identity in the picker, so more than one was always the bug. And if the AI names a label you don’t actually have — a rename, a deletion, or simply an invention — it adds it as new and says that is what happened, rather than reporting a replacement that never occurred.

v1.8.521

Brown Box and Padded Envelope are added properly now

Yesterday’s version wrote your answer into the Original Box comments, because adding a real accessory row looked like it needed a NOVA function the bookmarklet can’t see. It doesn’t. Add another is a plain dropdown, and it already carries Brown Box and Padded Envelope right alongside the White Box that Yes adds — so answering No and picking one now adds the same kind of row, in the same place, as answering Yes would have.

Two things it does carefully. Options are matched on their name, never on the id numbers behind them, so it can’t quietly add the wrong accessory if those differ anywhere. And it checks the row actually appeared rather than assuming: whether the list adds on selection is an inference from the markup, so it watches for the row for about two seconds and falls back to writing the comment if nothing shows up. Being wrong costs a fallback, not a missing accessory.

It also won’t add a second one if that packaging is already on the list.

v1.8.520

Answering “No” to the white box now asks what it does go in

NOVA’s popup asks “Do you want to add a White Box?” with Yes and No. Its No button calls the same function as the dialog’s X — so answering No records nothing at all, and the item is left marked Replace on Original Box with nothing anywhere saying what it actually goes in. The manual doesn’t cover this either; it stops at “select No”.

The tools now ask the obvious next question: Brown Box, Padded Envelope, or neither. It waits for NOVA’s own dialog to close and puts up its own prompt, so nothing depends on how NOVA builds or tears that modal down, and only the No button triggers it — dismissing a dialog with the X isn’t an answer.

Your choice is written into the Original Box row’s Comments, which is the field the manual already uses for SEE SPARES — the packaging decision recorded with the packaging. Add-only, like everything else these tools type: an existing comment is kept and the choice appended, never replaced, and the panel says what was written.

One honest limit: it can’t add a Brown Box row the way Yes adds a White Box one. That runs whiteBoxYesClick(), a NOVA function in a script the bookmarklet has no sight of, and calling into an internal function on a guess is how you end up with a row nobody asked for. If the comments field is the wrong home for this, it’s one line to move — the options and the destination are both stored rules now, not code.

v1.8.519

The manual parser can now rewrite the NOVA rules

v1.8.517 moved the Inspection Process Manual’s rules out of the bookmarklet and into stored data, so a reissued manual wouldn’t need a code change. This is the other half: paste a new manual into Check for manual updates and a changed grade ceiling or exact wording comes back with a real Apply button. Re-copy the bookmarklet afterwards and the NOVA tools are checking against the new manual.

The AI is never asked for the internal format — it quotes the manual’s own fault wording and the grade, and the app derives the words the rule matches on using the same normalisation that transcribed the existing ones. That is checked rather than assumed: all 90 ceilings transcribed from the v1.1 table re-derive identically from their own wording, so a ceiling added in a year’s time matches the way today’s do.

Grade Wizard faults and the battery table still need a real code update — those are source constants, not stored data, and it still says so plainly rather than pretending to apply something it can’t.

v1.8.518

Product facts send themselves on save

Saving an item in NOVA now sends its product facts straight to this app — no copy, no paste. The payload rides a URL the same way the nova tick already does, because a bookmarklet genuinely cannot write to another site’s storage. Three things it does deliberately: it only fires as a save actually goes through, never when the checks stop one; it reuses a single named tab, so a day of inspections leaves one tab open and not forty; and it hands focus straight back to NOVA, since the next thing you want is the next item.

It routes, it doesn’t write. The same router the clipboard route uses still works out which item it is and shows what the gear DB is missing, and nothing reaches the sheet without a click. Automatic delivery, not automatic writing.

Sensor Quality is now set to N/A on anything that isn’t a camera. A lens has no sensor, so the field has one right answer. The N/A option is found in NOVA’s own list rather than assumed, and it is only ever set from empty — an existing selection is a judgement and this doesn’t overrule it.

The manual parser has its own button on the rail. A reissued manual is something you paste once, not something you go browsing for — the modal says which manual version the NOVA rules are currently working from, so you can tell at a glance whether they’re behind.

v1.8.517

The rest of the Inspection Process Manual, and the rules are now data

Read manual v1.1 end to end and built the checks it was carrying that nothing checked. Battery doors — an untitled row left unticked now gets the “Missing Battery Door” note typed for it, and the deduction is spelled out: −30 for Canon, Nikon, Sony, Fuji and Panasonic, and never for a video camera, where the tools tell you to cost it with partexmanagers first. Third-party batteries and chargers — Replace ticked with no approved brand named, and no “unacceptable third-party version” sentence for the customer. Grade ceilings — 94 rules off the Decline Guidance table, read against whatever fault wording is on the page, including the five faults the manual declines outright. SEE SPARES — content in the Spares box with nothing linking it from the packaging comments, so the website never shows it. One accessory per line below #NOTINCLUDED#, because the site splits on newlines. Retailer copy — warranty and promotional wording pasted in from a product page. Plus the exact-wording rules for “Missing Figure 8 Lead” and the Mathorn/Hähnel spellings, and a cross-check against what the gear DB says a model actually shipped with, so a tripod collar sitting on a camera body stops telling customers one is missing.

The part that matters more than any single check: the manual gets reissued, so none of this is written into the bookmarklet any more. Every wording, threshold and grade ceiling is data the app holds and bakes into the bookmarklet when you copy it — which means a new manual can change the rules without a code change. What stays in code is only the shape of a rule (“an exact wording”, “a note required when a condition holds”, “a grade ceiling”), because that is what a reissue doesn’t change.

The grade ceilings were tested against the manual’s own approved note wording, which is not the same wording as its table — the table says “rear LCD” where the note says “rear screen”, and “Top or rear command dial” is an either/or that a first pass read as needing both. 33 of 34 real notes now resolve to the right ceiling. They are reported as checks, never as fixes, because the manual is explicit that these are maximum grades and not automatic ones — going lower is always your call.

Also fixed: the quote-page tool no longer asks for a “Grade due to” note on a declined item. There is no downgrade to explain — the decline reason is what the customer is told.

v1.8.516

The findings now appear on the NOVA page itself

An outline told you a field was wrong but not what was wrong with it, so you still had to look across to the panel and carry the answer back. Each finding’s own wording now sits directly under the field it belongs to, and accessory rows that will read as Not Included to the customer say so on the row. The panel is now an index you can leave collapsed.

It cannot change what NOVA saves. The notes are not elements — they are two data attributes on tags NOVA already has, drawn by CSS as generated content. Nothing enters the page’s DOM, nothing appears in any field’s text, and nothing can be picked up by whatever NOVA serialises on Save. Both attributes are removed when the tools are switched off.

Separately, a check was plain wrong and has been corrected: Original Box marked Replace was being flagged as a mistake. The manual says the opposite — a missing original box is marked Replace, and you then answer the White Box popup (No if it is too large, or needs a brown box). It is Original Packaging — a poly bag, wrapper or pouch — that is never Replace, because no replacement is supplied for something that never shipped in a box. The tools now flag that one instead, and remind you about the popup on the other.

v1.8.515

Escape no longer switches the tools off

Once clicked, the tools keep watching the page: re-checking as you change fields, keeping the outlines current, and stepping in at Save. They stop when the page navigates — which "Save Changes / Next" does — so they need clicking once per item.

But Escape used to disarm everything: panel gone, field outlines gone, Save no longer watched. Escape is also what closes a NOVA dropdown or autocomplete, so the tools could silently stop working mid-item with nothing to say so. Escape now only closes what's open — the detail list, or the Save warning — and never stops the watching. Turning them off is the x, which is deliberate and visible.

v1.8.514

The tools now work on a quote page too

Click them on a quote and they check every product for one thing: a grade lower than the condition the customer was quoted, with no "Grade due to" note explaining why. That gap is what the note exists for — the customer sees it in the quotation email — and a downgrade without one is the omission that comes back. Clicking a flagged product scrolls to it.

Fields are found by their labels (Condition, Grade, Inspection Notes) inside each product's own block, so it reads three fields and the product name and nothing else — never the customer block, the quote reference, the amount quoted or the margin. It changes nothing and sends nothing anywhere.

Checked against the real markup: a grade above the quoted condition passes, a downgrade with a note passes, a downgrade without one is flagged, a worded condition like "Excellent (9)" is understood, and an item not yet graded is left alone.

v1.8.513

Added rows were never actually landing

Adding a product to the gear DB has never worked — not once, since the day it was built. Your Gear tab has 75 blank rows scattered through it, inherited from the old sheet, the first at row 55. Google's "append to the end" decides where a table ends by walking down until it hits a gap, so it concluded the data finished at row 54.

The proof was in the sheet: 559 named rows and not one of them added by this app. It now reads the name column, finds the last row that genuinely has something in it, and writes to the next one — no guessing, and it lands at the bottom where you'd look for it.

Those 75 blank rows are still there and will confuse anything else that reasons about where the data ends. Worth clearing, but that shifts every row below them, so it should be done deliberately rather than as a side effect.

v1.8.512

The app was showing a stale copy of the gear sheet for up to a day

Tested the write path against your real sheet and it works — an edit from the app lands on the Gear tab correctly. What doesn't work is the other direction: the app cached its copy of the sheet for 24 hours, so if you edited a row in Sheets, the app carried on showing the old version for the rest of the day with nothing to say it was out of date. That cache made sense when the sheet was a static reference and stopped making sense once the app started writing to it and you started editing it.

It now refreshes after twenty minutes, and also whenever you come back to the tab — which is exactly the moment you return from editing the sheet.

The same test found a smaller bug: every save stamped FALSE into blank Recall and Import cells. Blank means "nobody has said", FALSE means "no", and they aren't the same claim — the very mistake that made the shutter-count auto-tick misfire. Blanks are now left alone.

v1.8.511

Firmware not required ticks itself on lenses too

The previous version held firmware back on the grounds that lenses do technically carry firmware. Danny's correction settles it: Wex never records firmware on a lens. The question is what the process captures, not what the hardware has — so both boxes now tick themselves on anything NOVA doesn't categorise as a camera, and the panel says which category it read.

v1.8.510

Why "tick nova in the app" wasn't working, and five more bugs

The tick was being undone by the sync. The tab opened, ticked, and saved — but that all happened before the cloud sync had finished loading, so the save didn't count as a local edit and the incoming cloud copy overwrote it. It only ever worked by luck of timing. Both NOVA hand-offs now wait for the sync to settle and then re-find the quote on live data, because the whole state object gets replaced when the cloud copy wins — which also meant "item changed" could report success while changing nothing.

"Copy product facts" works — verified end to end against a simulated inspection page, correct name, SKU, battery and charger. If it did nothing for you it was either an old bookmark or the browser refusing the clipboard silently. That refusal now puts the text on screen in a selected box instead of the console. And it now runs automatically when you click the tools, as you asked; the button stays, because re-capturing after you've tidied the accessory list gets a better "in the box".

On a lens, the shutter-count box now ticks itself from NOVA's own category — a lens has no shutter, and neither has a tripod or a bag, and that needs no gear DB. Firmware deliberately doesn't follow: lenses do carry firmware versions, so that stays a per-model fact.

Also fixed: clicking the tools twice left the first copy running (two panels fighting, "Save anyway" needing two presses); "Not the item quoted" could be typed mid-edit while the SKU box was briefly empty, and came back if you deleted it; removing the grey-import line flattened every paragraph break in the notes; a hand-typed hyphen made the same line count as missing; the "item changed" message never carried what the item used to be, so it always offered to add a duplicate instead of updating; and the auto-tick treated anything that wasn't literally "YES" as "no shutter count", which could have ticked not-required on a body that has one.

v1.8.509

The tools panel moved into NOVA's own nav bar

It used to float at the top right, where on anything under a very wide screen it sat on top of the Accessories card — and nine of its checks are about accessory rows, so it covered the thing it was pointing at. It now puts a small chip in NOVA's nav bar showing the count, colour-coded, with the detail hanging underneath when you click it.

It opens closed there, deliberately: the wrong fields are already outlined on the form, so the list is for reading detail rather than for telling you there is any. If the nav bar can't be found it falls back to floating — losing the tool because a selector moved would be worse than an imperfect position.

v1.8.508

The shutter-count auto-tick was finding nothing to tick

Checked in the browser against your real synced sheet and it was carrying zero exceptions, on a sheet with plenty of them. When a model has no obtainable shutter count the sheet says "NOT ATTAINABLE", but the app cleared that text on reading it — sensible on its own, except it destroyed the difference between "we know it can't be got" and "nobody has filled this in", and that difference is the entire basis for ticking NOVA's "not required" box.

The app now records whether the sheet said anything at all, separately from what it said, so a stated no is distinguishable from an empty cell. Silence still asserts nothing.

Re-copy the bookmarklet — the one you have carries the empty index.

v1.8.507

NOVA can now update the day's plan, and the nova tick folds in

Two more buttons in the pre-flight panel. Tick nova in the app does what the old separate nova-tick bookmark did — marks that quote's items nova-done here — so it stops being a fourth thing to install. Update the app: item changed appears once the product name and Parent SKU have both changed, and tells this app what's actually on the bench.

The app decides what that means, because only it knows whether that line is in today's plan and what the tracker consequences are. If the line exists it offers the substitute flow; if it doesn't, it offers to add it at your library's default time. Both go through the same paths as doing it by hand, so the change log, the allocation snapshot and any pending tracker writes all see it properly. Nothing is applied without a confirm.

Only the quote reference, product name and SKU travel — a message about a job, not about a person.

v1.8.506

One bookmark instead of three

The three NOVA bookmarklets are now a single NOVA tools bookmark that works out what applies from the page it lands on. On an inspection page it arms the pre-flight; on Canon's checker it fills the form; anywhere else it says so. The other two tools appear as buttons inside the panel that's already on screen — "copy product facts" always, "check serial" only on a Canon body that has a serial.

Neither of those fires by itself, deliberately: one overwrites your clipboard, which you use constantly for SKUs and phrases, and the other opens a tab. Both are one tap from where you already are.

Behind the scenes they stay three separate source files stitched together at build time. That matters: the capture's promise — that it never reads the serial, the notes or the customer block — is kept by that file not containing those things, and you can still check it by reading one file. Written as a single script with branches, that promise would have become something you'd have to take on trust.

Two additions to what it does. "Not the item quoted" is typed into the Inspection Notes when the product name and the Parent SKU have both changed since you opened the item — both, because a corrected typo isn't a different item and neither is a re-pasted SKU. It uses a baseline it takes itself rather than NOVA's own "name changed" flag, since nobody can currently say whether that flag trips from merely re-selecting the same product. And battery-health reminders moved out of NOVA's own storage, which gets wiped, into a synced setting here.

v1.8.505

Shutter/firmware tick themselves, battery health gets remembered — and two defects from today, one destructive

If the gear DB says a model has no obtainable shutter count, NOVA's "not required" box now ticks itself — same for firmware, which gets a new column in the sheet. The bookmarklet can't read the gear DB across origins, so the answer is baked in when you copy it from the banner: only the exceptions travel, a few hundred bytes. Whatever it ticks is announced in the panel, never silent. Re-copy after editing the DB, as with any change.

Battery health is learned, not assumed. The first time it sees a health figure like "0/4" in a battery's note, it remembers that you record them — on NOVA's own machine, nothing leaves the page. From then on a battery that's present without one gets flagged. Learned rather than hard-coded because a rule you didn't actually follow would fire on every battery every day, which is how a panel becomes wallpaper.

A review pass then found two defects in code shipped earlier today. One was destructive: arming the pre-flight passed the Grey Import checkbox's state to the note-syncing code, so arming a page where the box was unticked but the sentence was present — a reopened item, or a note typed by hand — silently deleted a line from customer-facing Inspection Notes. Arming now only ever adds; the reverse case is reported instead of corrected, because removing a sentence from a customer-facing field isn't this tool's call.

The other was a broken promise: the capture searches for a product name using a pattern generous enough to also match Sensor Specifics — per-unit free text about the item in front of you — which then went to the clipboard and was offered as a possible product name. That whole search is deleted rather than narrowed.

Also: Parent SKU being empty is now flagged, a Unique Product Note starting "Grade due to" is flagged as reading like an Inspection Note, and a capture now carries its own version so pasting a stale one tells you so.

v1.8.504

A 1-2-3 banner for the three bookmarklets

New strip under the tab bar, in the order one item actually goes through them: 1 Pre-flight when you open an item in NOVA, 2 Capture when the product isn't in the gear DB, 3 Serial check for Canon bodies. Each says when to use it and what it does, including the awkward bit — that the serial check needs two clicks on two tabs.

Each step's name is also its copy button, which is the point: these changed a dozen times in a day, and "re-copy it" is the fix for most of the ways they appear broken, so the fix now sits where the reminder is. Dismiss it once and it stays gone; Guide section 11 keeps the full instructions.

v1.8.503

Grey-import note is enforced, and the bookmarklets say which version they are

The grey-import line only got typed if you ticked the box after arming the pre-flight. Tick first, arm second, and nothing happened. It now catches up on arrival — if Grey Import is already ticked when you click the bookmark, the note goes in — and, more importantly, a missing line is now a FIX finding in its own right. So the rule holds whether or not the typing ever ran, and you can't save past it without deliberately overriding.

On the serial check not filling anything: the Canon half is working — tested against their live page, it fills the serial, resolves "5D IV" from their own model list, and the country is already set. The likely causes are all upstream, so both have been addressed. It takes two clicks on two tabs: once on NOVA to open Canon carrying the serial, then the same bookmark again on the Canon tab. Nothing fills until that second click, because a bookmarklet cannot reach into another site's tab. That message is now much harder to miss, and clicking it on a Canon tab with nothing to fill says so and suggests the other likely cause.

Which is that your bookmark may be an old copy. All three now stamp their version into every message they show, so a stale bookmark is immediately distinguishable from a broken feature — they changed a dozen times today and there was no way to tell them apart.

v1.8.502

The bookmarklet builder was corrupting text inside the scripts

Danny pasted his installed pre-flight bookmark back, and it contained '3rd Party(Unbranded)' — with the space eaten out of it. The tool that squeezes these scripts onto one line was using pattern matching that can't tell code from text, so it reached inside quoted strings and removed spaces next to brackets and commas.

That mattered. The check for third-party wording compares against that exact string, so it would have flagged every correctly-written note as an error and passed the incorrect ones — precisely the mistake that check exists to catch, inside the thing doing the catching. It had also turned blur(6px) saturate(1.3) into invalid CSS, which is why the panel's backdrop blur never appeared.

The builder now walks the source properly, tracking whether it's inside a string, a regular expression or a comment, and leaves text alone. It gives up a few hundred bytes of compression that were never worth a tool that can quietly rewrite what a script says. And it now checks every string containing a space against the output and refuses to build if any changed — 116 of them in the pre-flight alone.

All three bookmarks need re-copying from Guide section 11; the installed ones carry the corrupted text.

v1.8.501

The capture takes less and does more with it

The NOVA capture was collecting things about the unit in front of you rather than about the product — each accessory's received/replaced state and note, and the per-inspection WEX SKU — none of which anything used, and all of which contradict the rule the capture exists under. Removed. It also stops scanning page headings and the tab title for a product name once NOVA's real Name field has answered, since either of those can carry a customer name or a quote reference.

What it does keep now gets used. Pasting a capture can fill in the product type from NOVA's own category, whether a shutter count is obtainable, and what's in the box — from the accessory list, minus the battery and charger that have their own columns. The quick paste route and the full form now fill the same set; there was no reason for the faster one to know less.

Two corrections: the add form was prefilling product types that didn't exist in its own dropdown, so the sheet was collecting two vocabularies for one idea — buckets that don't map to a single category are now left blank rather than guessed. And the "→ DB" button on a past note now sends only the note. It was sending the whole line, including things like "declined (mould)" and "harder than expected", which are facts about one quote on one day rather than about the product — and which ended up in a shared workbook and, from there, in what Ask AI is told.

v1.8.500

The pre-flight panel, redesigned — and the wrong fields now say so themselves

The panel was hard to read, and the reason turned out to be the opposite of what I assumed. I had styled it to stand out against NOVA's white Bootstrap page — but your Stylebot theme already makes NOVA dark, mono, square-cornered and lime-accented, with the same border colour on every card. So it wasn't clashing, it was camouflaged: a dark box that read as one more card, whose most eye-catching element was its own title, in the colour the page uses for every hover and focus ring.

The biggest change: the form marks itself. Every finding now outlines the field it's about — solid red for a definite problem, dashed amber for something worth a look — and the outline stays until the finding does. The panel stops being a list you have to keep re-reading and becomes an index of things that are already visible.

The Save warning moved to where your cursor is. It used to re-render top-right, a full screen diagonal from the Save button you'd just pressed, listing everything including the things that hadn't blocked anything. Now a small panel slides in just above the Save buttons, showing only what's actually wrong, and the button you pressed flashes red so the refusal is obvious. "Show me" takes you to the first problem; the old label said "Let me fix it", which promised the one thing this tool won't do.

The panel itself now reads as an overlay rather than a card (translucent with a blur behind it, a brighter border), the finding is the headline instead of the word "PRE-FLIGHT", and severity is carried three ways at once — colour, filled-versus-outlined, and a bar down the edge — so it survives being read by anyone who doesn't separate red from amber. Rows are struck through and collapse away as you fix them.

v1.8.499

Serial check is for bodies, and now says so before opening anything

Since only bodies get serial-checked, the bookmark no longer opens Canon's page for anything else. It reads NOVA's own Product Category and stops with "serial checking is for bodies — NOVA has this as: Lens", saving a tab and a context switch. A blank category doesn't block it: missing information isn't the same as information that says no.

That also retires the lens-matching problem from the last version rather than working around it.

v1.8.498

The Canon model name is matched against Canon's own list

You were right that it doesn't match — and it's worse than a missing prefix. Canon's list has "600D", "5D IV", "5D MKIII" and "EOS R5" and "EOS R5 MK II". No rule produces all of those from a Wex product title.

But their page ships the list: a hidden dropdown of 404 model names the field validates against. So rather than copying their vocabulary into this app, where it would go stale, the whole Wex title is now carried across and matched against their own copy on their own page — longest match wins, with "Mark"/"MK" and "PowerShot"/"PS" reconciled. Checked against all 404: every body tested resolves correctly, including all three spellings.

Lenses are in their list too but written in a different language ("RF 100-500 4.5-7.1 L IS U" against Wex's "RF 100-500mm f4.5-7.1L IS USM"), which nothing can bridge safely — and a wrong model would check a real serial against the wrong product. Those now say so and leave you to pick from the typeahead.

v1.8.497

Canon serial check: verified against the live page, and it now stops at the tickbox

Tested end to end on Canon's real form. The autofill works — product name, serial and country all land and validate. But the page has an "I'm not a robot" tick above the button, which the first version didn't account for, so its automatic click would have done nothing.

It now fills the three fields, scrolls the tickbox into view, and stops there for you to finish. That was always the right split: retyping a twelve-digit serial is where mistakes happen, and ticking a box isn't.

One limitation worth knowing, from Canon's own wording on that page: the checker only covers products put on the market from 2015 onwards, so it can't answer for older bodies at all.

v1.8.496

The brief stops calling a guess a certainty

Your allocation paste has no SKU column — the SKU on a quote line is worked out by matching the name against your item library, and when that match isn't exact the item row has always said so with a "≈" and a "please verify" note. The brief ignored that flag and printed "Matched by SKU", its most confident label, on top of a guess. A guessed SKU now ranks below an exact name match, and when it is used the card says plainly that the SKU was guessed and is worth confirming. The NOVA capture router does the same.

Also closed a gap that predates this week: the camera info modal's Edit button, and the form behind it, write to a shared Google Sheet and had no viewer-mode guard — only Google's own permissions stood in the way. The form now refuses outright in viewer mode, the Edit button isn't rendered, the camera button is hidden from item rows, and the gear-DB sync controls are out of Settings. Settings itself stays open, so nothing legitimate is lost.

v1.8.495

Grey-import note types itself, and three ways the gear DB could lose data

Tick Grey Import on a NOVA inspection page with the pre-flight armed, and the standard sentence — "The item was found to be a grey import – 5% Deduction made" — is typed into the Inspection Notes for you. Untick and it removes that exact line again, only ever the line it wrote. This is the one thing the pre-flight types: the judgement is yours and already made, the sentence is boilerplate that always follows it.

A review pass over how information moves through the app then found three ways yesterday's convenience features could quietly destroy data. All three succeeded and reported success, which is what makes them the dangerous kind.

"+ fault" and "→ DB" rewrote the whole row from a cache up to a day old. Adding one fault line also rewrote battery, charger, tier and the entire fault list from that snapshot — so correcting a battery in the sheet in the morning and adding a fault at lunchtime silently reverted the battery, and a fault line added from another device was erased by the act of adding another. They now re-read the row and apply the change to what's actually there.

The catalogue matcher wrote to remembered row numbers without checking them — up to 559 rows in one request, breaking the exact rule added that morning for the single-row writer. It now re-reads the name column first and refuses to write anything at all if a single row has moved. It also spots when two gear rows claim the same catalogue product and leaves both alone, rather than giving them the same SKU and making every later lookup pick one of them forever.

The rename option is now off by default. Filling the SKUs is unambiguously useful; renaming changes the string everything else matches on, and only helps if the catalogue's title is also what the allocation paste says. Worth confirming before doing it to hundreds of rows at once.

v1.8.494

Canon serial check without retyping the serial

A third bookmark, and the answer to "how do we check this one" for Canon. On a NOVA inspection page it reads the serial and product name and opens Canon's own UK serial-number checker carrying them. Click it again on that tab and it fills the model, the serial and the country, and submits. Two clicks, nothing typed.

The reason isn't the clicks saved — it's that a serial is a long string of digits, and retyping one by eye is exactly how a wrong grey-import answer ends up recorded against the wrong camera.

It's the only tool allowed to touch the serial number, because putting it into the manufacturer's own checker is the entire point — the manual workflow minus the typing. The serial travels in the URL fragment, which browsers never send to a server, and it reaches nothing else: not this app, not the gear sheet. The capture bookmarklet still never reads serials at all. On a non-Canon product it says so rather than opening a page that can't answer; for those, record the method in the gear DB's "how to check" field and it appears on the item's brief.

v1.8.493

The add form only asks what the type actually has

Type is a dropdown now, using NOVA's own product category list, and it decides which fields the form shows. A lens loses Shutter count, Where to get it, Battery and Charger; an accessory loses Mount and Tier as well; a camera shows everything. Fields that are hidden keep whatever they held — nothing is discarded — and a NOVA capture sets the type straight from the page's own category, so the right fields appear before you've touched anything.

Sharing NOVA's vocabulary also stops the two systems drifting into different spellings of the same thing.

The pre-flight check now arms the page rather than just reporting once. Click it when you open an item and it keeps checking as you work, then steps in if you press Save with something definitely wrong — with a "Save anyway" whenever you disagree. If a check ever errors it gets out of the way and lets the save through; a tool that can jam Save would be worse than no tool.

v1.8.492

NOVA pre-flight check, and two real bugs the live markup exposed

New pre-flight bookmark (Guide section 11), for the moment just before you save an item. It reads the form and lists contradictions between things you have already entered: a grade above what the shutter count allows even for a pro body, sensor marks that affect images on a grade of 9 or better, a dust level that caps lower than the grade set, both Received and Replace ticked, third-party wording that isn't the exact approved string, a near-miss of the #NOTINCLUDED# marker, internal shorthand left in the customer-facing notes. It stays open and re-checks as you fix things, so the list empties itself, and clicking a line scrolls to the field. It changes nothing and presses nothing.

It also previews what the customer will read under "Not Included" — as a neutral line rather than a warning, because a genuinely absent strap and a row you forgot to delete look identical in the page and treating either as an error would make the panel noise.

Two capture bugs, both found by reading NOVA's actual markup. It was recording the wrong SKU: the "WEX SKU" field is empty on an inspection page, and the number that matches your allocation and the product catalogue is Parent SKU. And it was reading a third "Missing" checkbox on each accessory row that does not exist — there are only two, Received and Replace, and "not received" is the absence of both. Every captured row therefore claimed nothing was missing.

The Dust Level list is now NOVA's real one, all seventeen options in its own wording — including the four aperture-specific variants at the bottom that were previously guessed at. The grade caps for them remain marked unverified, because knowing the exact wording still doesn't tell us what each one caps at.

v1.8.491

The capture reads NOVA's actual Name field now

Danny supplied the real element — the field labelled "Name" on the inspection page. The capture reads that first, which ends the guessing: it was previously scanning for anything with "Description" in its id, and NOVA's name field doesn't have that word in it, so nothing matched and it fell through to headings and the tab title. The candidate-collecting fallback stays for any page that turns up without the field.

Re-copy the bookmarklet from Guide section 11 — the one installed before this is stale.

v1.8.490

Match the gear DB to the product catalogue

New "Match to catalogue" button next to the Camera DB sync in Settings. The migrated gear sheet carried 559 rows with an entirely empty SKU column, while the Product Lookup workbook already holds the canonical Wex product title and SKU for around six thousand products. This compares the two and fills the gap.

It sorts every row into three buckets by confidence: identical names (safe to apply in bulk), close-but-not-identical (a checklist you tick), and no match at all (listed so you can see what's genuinely missing from the catalogue). Optionally it also adopts the catalogue's exact product title — which is worth doing, because that's the same string your allocation paste uses, so every brief that opens afterwards matches more reliably. Writes go in batched, and nothing is written without you pressing the button.

v1.8.489

The NOVA capture was reading the wrong field for the product name

It was taking Preset Description, which NOVA generates from the Category and Grade you picked — so it described a condition, not a product. The app's own notes on NOVA's fields say exactly that, and the first version used it anyway.

It no longer looks at that field at all. Instead it collects every plausible candidate from the page — named description fields, headings other than the quote reference, the tab title — and carries all of them, so when more than one exists you get a dropdown to pick the right one rather than a wrong answer presented as fact. Matching was never affected: that runs on SKU, and a new DB row is named after the quote line, not after whatever NOVA called it. If you paste a capture back to me, the candidate list in it will pin the correct element permanently.

v1.8.488

The gear sheet has a proper schema

All 559 rows now live on a new Gear tab with plain column names and a layout built for the job: SKU near the front, the old "shutter count attainable" and "where" columns merged into one that reads "EOS UTILITY 3[PC1]" or "NOT ATTAINABLE", multi-line fields converted from comma-separated to one-per-line, and new columns for Wex category, what's in the box, notes, status and the date a row was added. IMPORT and IMPORT CHECK carried over intact.

The migration ran tab-to-tab inside the same workbook rather than rewriting the original, so the old Photo tab still holds the pre-migration data untouched. It is the backup, and going back to it is a one-word change. Nothing was deleted.

v1.8.487

A NOVA capture now works out for itself what it is

New paste icon on the left rail. Click the bookmark on a NOVA page, come back here, click that icon — and it reads the clipboard, identifies the product, finds which line on today's allocation it belongs to, checks whether the gear DB already knows it, and tells you the one thing worth doing. If the DB has the row and some fields are empty, it lists exactly what it would fill and does it in one click. If the DB doesn't have it, the add form opens already filled in. No hunting for the item first — the capture already knew what it was.

Matching is deliberately strict: exact SKU, then exact name, then it asks. Nothing fuzzy, because a wrong guess would enrich the wrong product's row in a shared sheet. It also only ever fills fields that are empty — a capture is evidence about a product, not an authority over something already checked by hand.

And the sheet's IMPORT / IMPORT CHECK columns are now read and shown: whether a product can be checked for being a grey import, and how you check it. That pairs with NOVA's own Grey Import box — the DB can tell you where to look before you tick it. Both are editable in the product form.

v1.8.486

Room for the reorganised sheet

Widened the columns the app reads from the gear sheet, ahead of the reorganisation. Groundwork only — nothing changes until the sheet does.

v1.8.485

Chargers resolve, cards read tighter, and the phone gets thumbs

Canon names a charger after the charger ("LC-E6E"); the approved-replacement table names it after the battery it charges ("LP-E6P/E6NH/E6 Battery Charger"). The two share no text, so a camera's charger quietly found no match and the brief showed a battery replacement with nothing beside it. Bridged with a small alias list, which also picks up the Nikon MH-24 and a couple of Sony chargers.

The "on this quote" block no longer repeats the scheduling chips already on the item row — it keeps only the lines that change what you do with the item in hand. And on a phone, the brief's buttons, chips and the capture input are now sized for a thumb, with the camera edit form dropping to a single column instead of squeezing two into 150px each.

Adding a new product from a brief now returns you to that brief, where the card you just filled in has data on it, rather than to a standalone card.

v1.8.484

Grow the gear DB from work you've already done

The fastest way to fill the DB isn't a form — it's the notes you're already writing. Every past note or finding on a brief card now has a small "→ DB" button that adds that line to the product's common faults in the sheet. And any card with a DB row gets a single input at the bottom: type "battery door latch weak", press Enter, done. No modal, no context switch, no fourteen fields.

Both write straight to the sheet through the verified-row path added a few versions back, and the card re-renders so you can see it landed. Duplicate lines are refused rather than piling up.

v1.8.483

The brief says how sure it is, and leads with what matters

A brief card finds its Camera DB row by fuzzy name match when there's no exact one — and rendered the result as settled fact. "Canon EOS R5 Body" and "Canon EOS R5 Mark II Body" match each other closely enough to slip through, and those two take different batteries and sit in different shutter tiers, so a silent near-match doesn't just show the wrong name, it shows the wrong part to order. Matching now tries SKU first, then exact name, then fuzzy — and a fuzzy match is called out in amber above everything it produced.

Card order reworked: recall first, then what's true about this line, then the battery grid with its approved replacement immediately under it, then what the DB knows about this specific item, and only then the collapsed reference tables anyone could look up. The tracker-category chip now appears only when the line is actually headed for the tracker — added, or missing — which is the only time it tells you anything.

Two smaller ones: new rows are appended into the next blank row rather than a freshly inserted one, so they inherit the column's dropdowns and formatting; and the add form no longer guesses a PRO/ENTRY tier from a minutes-only fallback that assumes anything 24 minutes long is a lens. A blank field beats a confident wrong one.

v1.8.482

The gear DB reads its own column headers now

The sheet parser used to address columns by position — first column is the name, fifth is the battery, and so on. Inserting a single column would have shifted every field silently: battery read as charger, faults read as a URL, no error anywhere. Since reorganising the sheet is exactly that operation, it now reads the header row and works out which column is which by name, accepting both the current headers and the plainer ones planned for the reorganisation.

Two things follow from that. Saving an entry now rewrites only the columns the app actually manages and puts every other cell back exactly as it found it, so columns it has never heard of survive an edit instead of being blanked. And the ambiguity in multi-line fields is settled by the schema rather than guessed: on the old layout a comma separates two bullets, on the new one it's just punctuation inside a single bullet.

v1.8.481

Four real defects in the gear-DB write path, found before they bit

A review pass over the new cataloguing code found four ways it could quietly write to the wrong place — all of which the planned sheet reorganisation would have triggered. None had caused damage yet.

The sheet range had no tab name. A Sheets range written that way means "the first tab", so every read and every write followed whichever tab happened to be leftmost in a workbook that has eight of them. Dragging any tab to the front would have made the app read an empty sheet, or overwrite a row on the wrong one. Now explicit.

Edits wrote to a remembered row number without checking. That number comes from a cache with a 24-hour life, so sorting the sheet, inserting a row or deleting a duplicate left it pointing at someone else's camera — and the next Edit would have overwritten that row silently. It now re-reads the row first, and if the contents moved, finds the camera again by SKU or name before writing; if it can't, it refuses rather than guessing.

Duplicate detection had three holes: it never looked at SKU (the likeliest duplicate is the same SKU under a slightly different title), it passed silently when the local copy of the DB was empty, and it checked exact names while the brief decided "not in the DB" using fuzzy matching — so near-misses slipped through the gap between the two. It now syncs before checking, matches on SKU first, and offers a near-miss as "did you mean this row?" rather than ignoring it.

Multi-line fields were stored comma-separated, so a single fault written as "sticky shutter, especially when cold" came back as two separate bullets. They round-trip on line breaks now, and existing comma rows keep working until they're next edited.

v1.8.480

NOVA capture bookmarklet — fill the gear DB by inspecting, not typing

A bookmark you click on a NOVA inspection page. It reads what that page already knows about the product — the accessory table, which is where the exact battery and charger models live, plus SKU, parent SKU, category, and whether a shutter count is even required — and copies it to your clipboard. Paste it into the box at the top of the "Add to gear DB" form and the battery, charger, SKU and shutter-count fields fill themselves in. Install instructions are Guide section 11.

It captures product-level facts only: never the customer block, never the quote reference, and deliberately not the serial number — a serial identifies one unit, means nothing in a catalogue of models, and has no reason to leave NOVA. The readable source lives in the repo at tools/nova-capture.src.js, with a checker that verifies the installed bookmarklet still matches it.

Also fixed: on a brief card, a RECALL warning was rendering below the "on this quote" block. It's the one thing that changes what you do before touching the item, so it now sits first.

v1.8.479

Catalogue gear as you inspect it — the Camera DB can take new rows now

The app could already edit a row in the Camera DB sheet; it could never add one, which is why that sheet only ever held cameras. A brief card for anything not in the DB now carries an "Add to gear DB" button that opens the same edit form pre-filled from what the app already knows — name, SKU, type from the item's category, tier from its Wex category, and a guessed lens mount — and appends it. Lenses share the cameras' tab and columns rather than getting their own: a lens just leaves the battery, charger and shutter cells blank, and in exchange every existing lookup (the fuzzy name match, sidebar Search, the brief itself) works on it from the moment the row lands. Names already in the sheet open for editing instead of quietly becoming a second row.

Same release, brief layout: a camera body's card carried three reference tables and ran three times the height of a lens's, which buried that card's actual findings and stranded the short cards beside a wall of numbers. The shutter, sensor-debris and Dust Level tables are now collapsed accordions — they're lookups you consult, not prose you read — while everything item-specific (faults, things to look out for, replacements, your history) stays open. The cards also flow in columns rather than a grid now, so an uneven card no longer leaves a hole next to it.

v1.8.478

NOVA Dust Level ladder alongside the sensor bands

The Dust Level dropdown is what actually gets set in NOVA once the sensor call is made, so its grade ladder now sits directly under the sensor-debris table on a body's brief card. Only two of those options are confirmed against the real dropdown — "Heavy dust and debris" and "Moisture and dust"; the rest are interpolated between those two anchors and carry the same "unverified" chip they already do in the Grading modal, with a note to check the wording in NOVA before relying on one.

v1.8.477

Sensor debris bands on the brief, for bodies

The sensor-debris criteria table now sits under the shutter bands on any camera body's brief card, including the digital-compact case and its NOVA Dust Level instruction, with the Decline row marked in red. Unlike shutter count there's no number to look up — severity is a judgement call on spread and aperture range — so it's the criteria verbatim, the same way the Grading modal treats it. Shown for bodies only; a lens has no sensor.

v1.8.476

More on each brief card: shutter bands, tracker category, item state

Six additions per item. The shutter-count grade bands for this camera's tier, shown only when the Camera DB says a count is actually attainable — that's the number going into NOVA and the tier is the only thing the Grading screen was adding. The Wex tracker category and column this line counts toward. Release year, as a wear hint. A plain-English "on this quote" block for things the item row only carries as one-character chips: no time set, your library default differing from the Wex allocation, an auto-adjusted paste, a substitution, charging, the current note or finding. "Seen N times before, last on DATE" even when nothing was recorded that day. And a footer of jump-offs — Grade wizard pre-filtered to the item, Ask AI about it, the Wex product page, the manual — plus a NOVA SKU link that falls back to the Camera DB's SKU when the quote line has none.

v1.8.475

The quote brief opens at a usable size

It was rendering at confirm-dialog width — a base .modal rule declared later in the stylesheet was quietly beating the brief's own width. It now opens wide, and lays the item cards out in two columns once there's room for them, so a five-item quote is one screen rather than a long scroll. Single column on a phone.

v1.8.474

Quote brief — everything on file for a quote's items, in one popup

New "brief" button on every quote header. It pulls together, per item, what the app already knew but kept behind four separate lookups: the Camera DB row (tier, whether a shutter count is attainable and where to find it, battery, charger, recall flag, common faults, things to look out for, manual link), the approved Mathorn/Hähnel replacement for whatever battery and charger that camera takes — with the same one-click copy as the replacement table — the Panasonic lens-code caveats when the item is one of those lenses, and your own history with that exact item: whether it usually runs over the allocated time, whether it tends to get declined and why, and the last few notes or findings you recorded against it. Read it before opening the box rather than looking things up once you're already stuck. Hidden in viewer mode.

v1.8.473

Battery / charger replacements have their own sidebar rail icon

The approved-replacement list no longer needs an accessory item in hand to reach — there's a battery icon on the left rail, under My performance, that opens the full table from anywhere. Opened that way it gets a filter box (type "EL15", "NP-W235", "LP-E6") since there's no item name to brand-filter on; opened from an item's name it still shows just that brand, as before.

v1.8.472

Approved battery/charger replacements are now one click to copy

The replacement part name gets retyped into NOVA every time a battery or charger is swapped, so the approved-replacement table now has a copy button on every row — in the Guide's Batteries & chargers section and in the brand-filtered popup you get from tapping an accessory item's name. Rows with no approved third-party equivalent copy the genuine part's own name, since that's what actually gets entered. The same 26 replacements are also searchable from the floating phrase quick-search now, so typing "EL15" or "NP-W235" anywhere in the app copies the right part without opening the Guide at all.

v1.8.471

Fixed a real bug in yesterday's "(NR)" tracker tag: it broke on the next read

A Fable planning pass caught this fast, before it caused real damage: v1.8.468's "(NR)" tag was written with a leading space ("12345 (NR)"), but the tracker's Quote Number cell is split on commas AND whitespace every time it's read — including by the Audit diff. That space fractured the tag into its own bogus token the instant the cell was read back, which would have shown up as the Audit panel flagging a tracker-only qid called "(NR)", and detached the tag from the real qid on the very next send to that cell. Fixed the tag to carry no internal whitespace ("12345(NR)", which the existing strip logic already handled fine), and added a one-time self-heal in sendTrackerAdjustment that reattaches an orphaned "(NR)" token to whichever qid immediately precedes it, in case a cell was already written with the buggy form today.

v1.8.470

Missing/removed button is now icon-only

The item row's "missing"/"removed" button was a text label; swapped for an icon (a question mark before it's flagged, a cross once marked genuinely missing, a minus-circle once marked removed from allocation) — matches the icon-only copy/Teams buttons already next to it in the same cluster. The full wording still lives in the button's tooltip/aria-label for anyone tabbing through or hovering.

v1.8.469

Grind gamification tab removed — extracted to a standalone project

The Grind tab (idle bot farm, Slayer Master, equipment/quests, the isometric map) is gone from this app. It had grown to roughly 16% of this repo's whole public/ JS+CSS on a feature used rarely, so it's been pulled out into its own standalone project instead of continuing to carry that weight here — not deployed publicly yet, so there's no live link to give. The skill-XP/levels layer it shared with the Strengths tab (per-skill 1-99 OSRS-curve levels, badges, the level-99 skill cape) stays exactly as it was; only the bot farm/Slayer Master/map half left. public/grind-core.js and public/grind-view.js are deleted, the Grind tab button and its modal are gone, and the AI bot-naming Cloud Function (generateBotName) is removed from source (a manual firebase deploy --only functions is still needed to actually undeploy it). No live-browser verification was possible for this change (Firebase-authenticated, no local run path) — every touched code path was re-read carefully instead, specifically checking for the cross-file dangling-reference failure mode a past Grind file-split caused a real data-loss incident with (see CLAUDE.md).

v1.8.468

Distinguishes "missing" from "removed from allocation for another reason"

Real feedback: the Missing button was doing double duty — genuinely missing items, and items you're removing from your allocation for another reason (e.g. giving one back) but that need the same tracker -1. Both used to be filed identically as "missing", which made the Teams message wording wrong for the second case and gave the tracker no way to tell them apart. Marking an item missing now asks which one it is (skipped entirely when un-marking) — the tracker-side effect and the app's own bookkeeping stay identical either way, but a genuinely-missing send now tags the qid with a trailing "(NR)" in the tracker's Quote Number cell (never for a plain removal), and the Teams message wording matches whichever reason you picked. The qid tagging is matched/deduped/reversed by the bare qid underneath the tag, so it's fully compatible with every qid already sitting untagged in the tracker. (Builds on v1.8.465, which separately fixed an all-missing quote never being able to finish.)

v1.8.467

History tab: the 12-week heatmap is now a real calendar

Real feedback on the History tab's heatmap: "hard to read, make it look closer to a calendar or more obvious what it's showing" — specifically the orientation (weeks as columns, no date numbers, hover to find out which day a cell even was), weekends rendering as grey "no data" holes in every column, and the colour code not landing even with the legend. Rebuilt as an actual month grid: one block per calendar month (this month plus the two before it, side by side on desktop, this month on top on a phone), rows are weeks, columns run Mon to Sun, and every cell shows its date. Each archived day shows its real target % in the cell and a fill bar along its bottom edge whose length is the %, so the green/lime/amber is reinforcing something you can already read rather than a code to decode. Saturdays and Sundays are dimmed as non-workdays instead of looking like a missed day (a weekend you did work still shows normally); a weekday with no archive keeps the empty-slot look so a gap in a working week stays visible. Today is outlined. Each month header carries its own "N of M on target" count, the "Last 7 archived days" summary and legend are kept, and clicking an archived day still jumps to its card below (now also works from the keyboard). Not yet checked in a live browser — the grid placement was traced in node against real 2026 dates instead.

v1.8.466

Ask AI Fable pass: honest errors, a cut-off guard, follow-ups keep their camera

Fourth Fable pass, back on Ask AI itself — the Cloud Function, the Chat tab, and every entry point into it. Same question as the previous passes: is this serving the real job well? Verdict on the shape: still right-sized. The privacy boundary, the owner-only gate, the daily cap and the v1.8.452 prompt caching all checked out (the knowledge base is byte-stable across a session — nothing in it varies spuriously). What a full read of the client and server found was a set of ways the feature was quietly less honest with Danny than it should be, mostly at the edges: what it says when something goes wrong, and what it does with an answer that got cut off. Every finding was verified against the code — one against the Firebase SDK's own source — before touching it. No live-browser or live-API pass was possible here; same caveat as usual.

1. Error toasts said "internal". Verified in firebase-functions-compat 10.13.2: a network failure (offline, function unreachable) maps to code internal with the message literally "internal", and the client timeout gives "deadline-exceeded" — every AI caller toasted e.message raw, so that's exactly what Danny saw. New shared _aiErrorMessage() (used by Chat, reconcile, self-audit and triage alike) turns the bare-code cases into "Couldn't reach Ask AI — check your connection", an offline-specific line, a timeout line, and a sign-in-expired line. Real server messages (owner-only, daily cap) pass through untouched.

2. The server told the truth about Anthropic errors, too. Every non-OK response from the API collapsed to "returned an error — try again shortly" — wrong advice for most of what it covered. callAnthropic now reads Anthropic's typed error.type: a rate-limit/overloaded response (429/529) gets one automatic short retry and, failing that, an honest "busy right now, wait a moment"; a rejected API key says the secret needs checking and retrying won't help; a 404 says the pinned model ID has probably been retired and names it. It also now logs usage — token counts only, never content — which is the only way the v1.8.452 prompt caching can actually be checked: cache_read_input_tokens staying at 0 across a thread means a silent invalidator. Until now nothing surfaced that at all.

3. A cut-off answer could poison the knowledge base. The two trailing sentinels the prompt asks for — PHRASE: (copied verbatim into NOVA) and FEEDBACK_NOTE: (auto-saved as a permanent note, sent on every future call) — sit at the END of the reply, so a max_tokens cutoff could hand over half a phrase as "the exact approved wording" or save half a sentence as fact. The function now returns truncated; Chat shows a "hit the length limit" notice on the turn, skips the phrase copy-chip only when the phrase was the last thing in the reply, and never auto-saves a note from a cut-off answer (shows it instead, for manual adding if it's right).

4. Follow-ups lost their camera. The thread exists so "what about its battery?" works without restating everything, but the per-question camera-DB lookup only ever read the current message — the moment a follow-up stopped naming the camera, the structured row (battery, shutter-count location, common faults) silently dropped out. It now falls back to the most recent camera named earlier in the same thread, labelled as such so the model knows it's "probably still this one" rather than "named just now". Still outside the cached block; a follow-up that names a different camera still wins outright.

5. A dead-end button. The bulk-paste confirm (450+ chars) offered "Ask as one question" even for text over the server's 500-character question cap — a guaranteed rejection (nothing was ever spent, the 400 comes before the counter), after which the error path restores the text and Enter loops straight back to the same confirm. It now says what the limit is and only offers the path that works.

Deliberately left alone: the daily counter's read-then-write isn't a transaction (two truly simultaneous calls could overshoot the cap by one — it's a runaway-loop backstop, not a budget, and calls are sequential from one client); its "day" rolls at midnight UTC, an hour off BST; and the sentinel parsers don't tolerate the model bolding a sentinel line with markdown (plausible given v1.8.451's observation, not seen live). functions/index.js changed — needs a manual firebase deploy --only functions before points 2 and 3 are live; the client is fully backward-compatible with the currently deployed function in the meantime.

v1.8.465

Fixed a real bug: an all-missing quote could never be finished

Real bug report: mark every item on a quote missing (giving the whole thing back before ever starting it) and the quote just sat "open" forever — no Finish button, no auto-resolve. Two bugs stacked: quoteAllItemsResolved required at least one non-missing item to even consider a quote resolved, so an all-missing quote's list of live items was empty and failed that check outright (should have been vacuously resolved). And the Finish button additionally required q.startedAtMin != null, a check quoteAllItemsResolved itself, userFinishQuote, and the header's ready-to-finish count/"Finish all" never applied — so even a quote that did clear the first check couldn't be finished unless it had been explicitly Started first. Both fixed; the button now agrees with the rest of the app.

v1.8.464

Phrase quick-search gets a "Most used" shortcut

Real feedback: even with the phrase quick-search's fuzzy matching already fast, your single most-reused phrase ("Grey import") still meant typing it out every time. Reference phrases and Grade Wizard fault wording now track usage (a copy via the search results row, the Enter-key shortcut, or the new shortcut itself all count), and the Phrases sidebar tab shows your top 3 most-used as one-tap copy buttons right above the search box, before you've typed anything — it hides itself while you're actively searching and comes back when the box is cleared.

v1.8.463

Core workflow Fable pass: two stale tick paths fixed, Stats made honest, day-window fixes

Third Fable pass, this time on the day-to-day flow itself — Today, Audit, Stats — asked the same question the Ask AI passes were: is this serving the real job well? Verdict on the shape of it: yes. The header strip, now-line, active/next chips, resume chip and ready-to-finish nudge already do the orientation work an ADHD day needs, and Audit's send/verify/reversal path is careful. What a full read-through did find was two ways of ticking an item that had quietly fallen behind the three-phase (inspect/Nova/admin) model, plus a handful of numbers and labels that disagreed with each other. Every finding below was verified against the code before touching it. No live-browser pass — same caveat as most batches here.

1. The Space shortcut couldn't finish an item. It ticked inspect, then went straight to admin — no Nova step at all, so an item driven purely by Space sat at inspect+admin forever (itemIsFullyDone needs all three), with no per-tick timestamps, no start stamp on the quote, no XP/undo. It now drives the real checkbox (.click() on the rendered row), so the one canonical tick handler runs — all inspect, then all Nova, then all admin, with the help text updated to say so. Enter on a focused quote now does exactly what the Start button does (front of queue, auto-pause the other) instead of a quieter stamp-in-place.

2. The item right-click "Tick done" was a no-op. It flipped only the cached item.done flag, which nothing on a live item reads — row class, progress bar, finish gate and drift all derive from the three phase flags. Visually nothing happened, and done was left desynced until the next real tick recomputed it. Now "Mark item done" ticks every outstanding phase with timestamps, XP and Slayer hooks (same as the checkbox path), and "Untick item" clears them with the same undo toast an accidental checkbox untick gets. "Mark all items done" gained the XP/Slayer calls "Mark all admin done" already had — Slayer progress had no boot self-heal, so it was being silently missed on that path.

3. Stats "Today's progress" now matches the header. Its tooltip said "item-progress-weighted minutes"; the number was binary (fully-done items only). Every other today-% surface — the Plan bar, the Target strip, the EOD hero, archived doneMin, and the history average the card compares itself against — is weighted, so Stats could read 40% while the header said 62% for the same state. The "Quotes done" tooltip likewise claimed to count effectively-done quotes while the code counted only FINISHed ones; the tooltip is now truthful and the sub-line shows "N ready to finish" so the gap is visible instead of implied.

4. Day-window honesty. Time-rail hour ticks were a hardcoded 08..16, so a late-start day (the day-start override slides the whole window) drew ticks above the rail and none for the hours it actually ran into — now derived from the real window. The Projected-end tooltip hardcoded "(16:30)"; the "Items today" tooltip said missing/declined counted as done when missing items are excluded from both numbers and declined only counts once admin'd. Both fixed.

5. Small real bugs. "Unstart" checked i.inspect (no such field — it's inspected), so a quote whose only progress was inspect ticks skipped the "this discards elapsed-time accounting" confirm. The "set start time" prompt on an unstarted quote ignored Nova-only and decline-only progress. And the "Paused Xm" chip on a paused card only updated on a full re-render — it now refreshes on the same 30s tick as the live-pace pill, so it can't sit at "Paused 3m" for half an hour.

Left for Danny: the card's Finish button requires a recorded start while the header's ready-to-finish count and "Finish all" don't (only reachable via Unstart on a resolved quote — a judgment call on which gate is right); the Stats time-of-day chart still buckets a hardcoded 08..16 (analytics-only, drops late-start hours); and whether Space should work phase-by-phase across the quote (as it always has) or item-by-item.

August 2026

v1.8.462

Inline tab-to-complete on four text fields

New shared helper, wireGhostAutocomplete() — the same technique browser address bars use: type a prefix of a known phrase and the remainder appears pre-selected in the field itself, no overlay or font-metric matching needed. Tab accepts (collapses the selection); typing over the selection or hitting Backspace/Delete rejects it, both for free from native text-field behaviour — nothing had to be built for either. Wired into four fields: the Decline item modal's note (suggests from Reference Phrases), the NOVA offline entry form's Notes field (same source), the "+ Add item" name field (suggests from the same item-library/catalogue results already powering its existing dropdown — Tab now completes the top match instantly alongside the arrow-key/click picker), and Ask AI's question box (suggests from questions already asked this session, so re-asking a near-identical one is one keystroke).

v1.8.461

9 new decline reasons — real recurring ones, not from the manual this time

Reported live as "decline reasons not showing in the dropdown" — turned out not to be a bug: Danny had always used "Other" + free text for anything the 5 manual-derived built-ins didn't cover (mould/mite/battery bulging/barrel movement — confirmed the manual's own "Declined Reasons Customer Friendly" section is fully covered already, nothing new to add from it). Went through the real "Found in past declines" list together — dozens of entries, many the same real fault typed differently each time. "bent cf" / "bent cf pin" / "cf pin issue" / "CF issues" are one fault typed four ways; "ntiq" (used 8×, more than any other entry) turned out to mean "not the item quoted, not an item we accept" once Danny explained it.

Added 9 new built-in reasons — the ones with genuine recurrence once near-duplicate wordings are combined, or explained directly: Not the item quoted, Does not power on, Battery recharge rate below standard, Displays an error code, Debris in optics affecting images (this one actually is in the manual's own decline table), Bent CF card pin, Item freezes/unresponsive, Autofocus does not function, Screen/LCD failure — each with matching customer-facing wording. Deliberately left out the true one-offs from the same list (missing joystick, aperture band loose, iris stuck, and a handful of entries too vague to safely codify without more context) rather than permanently bloat the dropdown for a single historical use — built as custom reasons later if they ever recur. Built as proper built-ins rather than per-user custom entries specifically so they can't be lost the way custom decline reasons were once before (see the sync-wrapper incident note at the top of this file's own history).

v1.8.460

Fixed a real bug: Ask AI started 400ing with "context payload is too large"

Reported live, right after the last two releases. Root cause: MAX_CONTEXT_LEN (functions/index.js) caps _buildAiKnowledgeBaseText()'s output at 60000 characters — a limit already raised once before, in v1.8.384, for exactly this reason. Measured the actual current knowledge base by running the real function in a stubbed Node harness rather than guessing: ~55,700 characters of static content alone, before any of Danny's own custom decline reasons or saved AI notes — within about 7% of the cap even at rest. The ~75 Reference Phrases and the new WORKFLOW_SCENARIOS block added across the last two releases were enough to tip it over on an ordinary question.

Raised MAX_CONTEXT_LEN to 150000 — same "well past any realistic near-future growth, input tokens are cheap regardless" reasoning v1.8.384 used, since Reference Phrases/GRADE_WIZARD_FAULTS/WORKFLOW_SCENARIOS are all actively-growing data the manual-reconcile tooling is expected to keep adding to. Three other caps on the exact same payload shape (MAX_KNOWLEDGE_BASE_LEN, MAX_CURRENT_DATA_LEN, MAX_GAP_CURRENT_DATA_LEN) were separate literals manually kept in sync with MAX_CONTEXT_LEN — now derived from it instead, so a future raise can't miss one of them the way this happened twice already. The client-side live-size warning in Guide's "What Ask AI knows" section (AI_CONTEXT_HARD_CAP) is updated to match, so it stops reading as dangerously full when there's actually comfortable headroom.

v1.8.459

A second Fable pass: closing more AI-blend risks, one provenance system, structured workflow data

Follow-up to yesterday's manual reconciliation — asked Fable to plan how else the manual's information could be better used, whether the AI reasons well over it, and whether "definitely in the manual" vs "AI inferred" could be made more obvious. Verified every finding directly against the manual before implementing. One flagged risk (non-monotonic viewfinder dust grades) turned out to be a text-extraction artifact, not a real bug — confirmed the existing data was already correct rather than "fixing" something that wasn't broken. The rest were real: the Corrosion category has five "light corrosion" rows mapping to four different grades depending on location (battery terminals 9−, external metal 8, battery compartment 7, pen-resistant 7) — same near-identical-wording-different-severity shape that already caused two blending bugs, now disambiguated with note fields. Same treatment for moisture (the manual's rule is "any moisture = 9−, only heavy drops to 8" — now stated explicitly so the plain-wording phrase isn't read as an unknown case) and front-optic scratches (the Max Grade table only grades front-optic scratches; approved wording exists for rear-optic too, but the grade genuinely isn't documented — noted so the AI won't assume symmetry). Two grip-rubber phrases ("peeling"/"discolouration" with no light/heavy qualifier) were genuinely missing with no documented grade — added as reference-only. Also added a lightweight boot-time check (_auditGradeWizardFaultsShape, console-only) that flags categories with 3+ rows and no disambiguating notes, or near-duplicate fault wording across different categories — the exact shape that's caused every blending bug so far, checked automatically instead of only when someone happens to notice.

Provenance: three different ad-hoc flags (confirmed: true/false on Dust Level grading, unconfirmed: true on Panasonic lens codes — opposite polarity for the same concept) unified into one source: 'judgment' field, now also applied to the two GRADE_WIZARD_FAULTS rows that were already documented judgment calls but only in code comments, invisible at runtime. Renders as the same small chip everywhere (Grade Wizard results, the Reference modal, Guide's lens-code table) instead of three different visual treatments (one of which was just italic text with no chip at all). Ask AI's "Not documented — my best guess: ..." prefix — previously pure prompt-intent, never actually detected client-side — is now parsed the same way the GRADE:/PHRASE: sentinels already are, into a real visible amber chip instead of text you had to notice yourself mid-answer.

Structural: the accessory/packaging scenario rules added yesterday started life as a flat list of lines.push() calls inside the AI knowledge-base builder — real data with no actual data structure, so nothing else could reuse it. Promoted to a proper WORKFLOW_SCENARIOS array (same pattern as GRADE_WIZARD_FAULTS), each row also carrying which Guide tab section covers its full process — metadata for a future jump-link, not wired into the UI yet. Full de-duplication of the three places approved wording currently lives (Reference Phrases, fault phrase fields, Guide prose) was assessed and deliberately deferred — a real risk Fable flagged, but a bigger, more careful refactor than fit this pass; noted as follow-up work rather than rushed.

v1.8.458

Reconciled grading data against the real Inspection Process Manual v1.1

Danny added the actual v1.1 manual (a Word-exported HTML file — the real source, not a pasted fragment). Ran a Fable-model pass reading it in full against the app's grading data, then implemented the findings. Fixed a real contradiction: the manual's Max Grade table has a standalone row, "Marks on the sensor visible in captured images" → 8 — the app had this backwards (a prior session, working from a fragment, had guessed it was the no-effect case). Corrected, with a note making the precedence against the separate f-stop-measured sensor debris ladder explicit both on the fault row and in Ask AI's own knowledge-base text. Also fixed the weather-seal grade-7 phrase (was using the wrong nick-severity wording) and closed a real AI-reasoning risk: the "Dust" category quietly contains THREE separate severity ladders (lens/optics, viewfinder plain-dust, viewfinder dust-and-debris) that diverge at identical severity words — added note fields naming each ladder so the AI can't blend across them, the same fix pattern used for the dial-skip ladder bug found last month.

Added ~75 Reference Phrases straight from the manual's "Approved Unique Notes"/"Approved Inspection Notes" sections — weather seal variants, lens scratch/debris (front/rear, light/large, "affects images" variants), grip-rubber-missing locations (×8), cosmetic cracks (body/card door/lens barrel/filter thread/pop-up flash, light/heavy), port/door/housing faults, serial number scenarios, standalone-battery decline wording, and customer-facing decline text for mould/lens mite/battery bulging/lens barrel movement (none previously had approved customer-facing wording, only internal dropdown labels). None invented a grade the manual doesn't actually state — where a phrase exists with no matching Max Grade table row, it's Reference-Phrase-only. Also added a new structured "accessory/packaging scenario → required action" block to Ask AI's knowledge base (White Box popup, battery/charger/AC-adapter/Mathorn handling, missing battery doors, bundled filters) plus a matching system-prompt rule, since these branching processes are exactly the shape of thing an LLM can flatten into one over-generalized wrong answer. One item stayed untouched on purpose: the manual gives the battery-door £30 urgent-deduction brand list twice with two different lists (5 brands vs 3) — genuinely self-contradictory in the source, so the app's existing list is left as-is pending Danny confirming with the manual's author.

v1.8.457

Fixed a real bug: pausing a quote and switching to a new one broke pacing/projected end

Reported live: "if i pause a quote and move onto a new one, the projected end and pacing doesnt add up properly, it thinks ive completed the previous quote." Ran a Fable-model pass over the whole timing engine to find it. Root cause, in computeSchedule(): a paused quote's occupied window was set to Math.max(plannedEnd, pausedAtMin) — which for a normal early pause is ALWAYS plannedEnd, i.e. the quote kept its full nominal window even though only the pre-pause portion was actually worked. The quote you switch to starts at its own real clock time, which falls DURING that still-inflated window, so the two quotes' windows overlap — and the schedule's cursor then jumps backward, compressing every quote after it and silently dropping the paused quote's unworked remainder from the day's plan entirely. computeDayStatus()'s drift math (which sums each quote's overlap with "now") then double-counts the overlapping span, so pacing reads behind even at a perfectly normal pace, and the projected end jumps earlier the instant you switch — exactly "thinks I've completed the previous quote."

Fixed: a paused quote's window now ends at the true pause point, and its still-owed weighted remainder (quoteRemainingMinutes — the same figure used everywhere else for "how much is left") gets pushed onto the schedule's cursor as pending work instead of vanishing, so every quote scheduled after it — and the day's projected end — correctly carries the debt forward. autoSort()'s own cursor seed had the identical bug (worse: it also blended in "now", which for a quote paused a while ago would shove every unstarted quote's suggested slot too far forward) and got the same fix. Also: projectedEndMin is now floored to the current time whenever real work is still outstanding, so a "projected end" can no longer read as a time already in the past; and the day-status strip's Pacing block now shows a one-line note when a quote's paused ("Q123 paused, 42m owed") so the (now-correct) drift number doesn't read as mysterious.

v1.8.456

"My performance" modal: a Fable design pass, then a real layout rebuild

Yesterday's modal (v1.8.455) was built quickly and never critiqued — asked for a Fable-model design pass on it, then implemented the findings. The Summary table's current-month highlight was inverted: .ref-grade bolded every OTHER month and .ref-row-match stripped it specifically off the current one, the one row that actually matters (both classes were borrowed from Grading's single-match-row table, where that combo means something different). Fixed with a dedicated highlight instead. Also: future zero-activity months no longer render as noise (0 lines, literal "#DIV/0!" in the rate column) — the list now stops right after the current month; numeric columns are right-aligned with tabular figures; % of target gets an ok/warn tint; the Month column is sticky so it stays visible while a narrow screen scrolls the rest of the table sideways.

The Feedback tab's Errors/Advisories tables were the bigger problem — stretched to 6 columns of long free text (reason + a nested specifics sub-line) on a table shape built for 4 short reference columns. Rebuilt as stacked cards: one small mono meta line (date/quote/deduction/status-or-month) plus full-width item and reason lines — no horizontal scroll needed at any width, and it drops a dead "Status" column that was blank on every real row. The advisory leeway count ("4/5 this month") — arguably the single most useful number in the whole modal — was buried mid-sentence below 25 rows of errors; it's now a stat card pinned at the top of the Feedback tab, alongside a total-errors count. Smaller fixes from the same pass: the "not synced yet" icon no longer looks like a close/X button, and the three "Open sheet" links (Data tab + Integrations) share one CSS rule instead of repeating the same inline style five times.

v1.8.455

Sheet IDs hardcoded; a new "My performance" panel pulls the tracker's Summary/Feedback tabs

Two asks. First: the tracker workbook and Product Lookup sheet IDs used to be settings-synced (v1.8.263) and kept getting wiped by a stale-cloud-snapshot race that a v1.8.408 patch never fully closed — Danny had to re-paste them after some refreshes. Both are now hardcoded constants (TRACKER_SHEET_ID / PRODUCT_LOOKUP_SHEET_ID), the same pattern the Camera Database sheet already used — the ID-entry fields in Settings → Integrations are gone, replaced by an "Open sheet" link, and Settings → Data gained a "Quick links" section with direct links to all three workbooks. Second: a new "My performance" modal (rail icon next to Grading) reads two tabs from the tracker workbook read-only — Summary (monthly lines/time/target%/errors/error rate) and Feedback, which is actually two independent logs sharing the same rows: real scored mistakes, and an advisory-only log (5/month leeway before they'd count as a real error). Synced manually or automatically when stale (daily TTL), cached locally like Product Lookup/Camera DB.

v1.8.454

Fixed a real bug: the self-audit tool (and likely manual-reconcile too) silently failed on every call

Ran the Guide tab's "Check for contradictions" self-audit tool live for the first time since it shipped (v1.8.395) — it had never actually been run and reported back. Found a real bug: every call threw "The AI service returned an empty response," reproduced twice in a row. Root cause in callAnthropic (functions/index.js): it read data.content[0].text, assuming the first block in Anthropic's response is always the text block — but the Messages API's content is an array of typed blocks with no such guarantee, and Claude Sonnet 5 (the model this tool and reconcileManualUpdate both use) can lead with a non-text block, leaving content[0].text undefined while the real text sat later in the array. Haiku (used by the everyday chat feature) apparently never hit this, which is exactly why it went unnoticed — the bug only shows up on the two MODEL_STRONG-tier calls, and reconcileManualUpdate uses the identical code path, so it was almost certainly silently broken the same way this whole time. Fixed by filtering for actual text-type blocks instead of assuming a fixed position.

Also: extended v1.8.452's prompt caching to reconcileManualUpdate — Guide's own copy now explicitly invites splitting a large manual into chunks (v1.8.451), and every chunk was resending the identical ~40-60k char current-app-data block fresh, the same latency tax already fixed for Ask AI's chat follow-ups. And a real mobile bug, found by a live QA pass earlier this session but not fixed at the time: the Today tab — the one used all day — scrolled the whole page sideways at phone width (~260-290px overflow), hiding every item action button (decline/missing/grading/delete/etc.) past the right edge. Root cause was a CSS grid track defaulting to minmax(auto, 1fr) instead of shrinking to the viewport; fixed with min-width:0 on the layout grid plus letting item rows wrap onto a second line instead of forcing the page wider.

v1.8.453

Grading data: reconciled against the real v1.1 manual, plus a live-confirmed AI comprehension bug fixed

Danny pasted the actual current Inspection Process Manual (v1.1) in full. Reconciled it against GRADE_WIZARD_FAULTS/the phrase library/Guide's procedural sections (a background pass, ~85 of ~92 Max Grade table rows already matched from the v1.0 transcription). Added what was genuinely new or missing: a "chip on the optic — affects images" grade-7 variant (the phrase already existed as an approved Reference Phrase but had no grade anywhere, so Grade Wizard's own search couldn't find it and Ask AI had no grounded fact for it), a new Card slot category ("Bent CF pin, dual-slot body" → 6), a new Weather seal category (3 tiers), a new Lens barrel category ("loose, any section" → Decline), and three new selectable DECLINE_REASONS (live lens mite / battery bulging / lens barrel movement — all three already had exact customer-facing wording as ordinary phrases, just no dropdown entry). Split the viewfinder dust ladder into two — the manual describes separate "dust" and "dust & debris" scales that diverge at the same severity word (Heavy dust caps at 9−, Heavy dust-and-debris caps at 8) — the old single merged ladder couldn't represent that. A few items were genuine judgement calls, not silent guesses — asked Danny directly rather than pick: kept an orphan grade-7 dial-skip row with no v1.1 counterpart rather than delete unverified grading data, and left the Grade-10-with-3rd-party-battery cap at 9+ per his call.

Separately, a Fable-model pass specifically probed whether the AI's EXISTING knowledge base (not what's missing — how well the AI actually reasons over what's already there) live against production, and caught a real bug: asked about a command dial that "skips every now and then, maybe once every 15 turns," which per the data is the best (grade-9) frequency tier — the AI answered grade 7 (two full grades too low), blending two different tiers of the same frequency ladder into one wrong answer, with the grade-9 row's own approved phrase attached to the wrong grade, presented as documented fact. Root cause: nothing in the knowledge base told the model these five rows were one measured-frequency ladder that must not be blended. Fixed at three levels: the ambiguous "occasionally skips" row got a `note` field explaining it's specifically the unmeasured-frequency case (a new field on GRADE_WIZARD_FAULTS, now serialized into Ask AI's context and grouped under real category headers instead of one flat list); the system prompt gained an explicit "these are a severity/frequency ladder, never blend adjacent tiers" rule; and a related root cause — an approved phrase existing was being treated as proof a grade was also documented, even with no grade anywhere for that specific case — got its own explicit system-prompt rule too. Also fixed while auditing: a self-contradictory sensor fault row (the fault text said "visible in images", its own phrase said "no effect on images"), a seeded Nikon battery phrase using Fujifilm's /5 scale instead of Nikon's real /4, a "heavy" grips-discolouration phrase missing the word "heavy" entirely, and "dust mite" vs "lens mite" terminology drift between two phrases for the same fault — all four also got matching migration patches so anyone with the old wording already saved gets it corrected in place, not just fresh installs.

v1.8.452

Ask AI: five real improvements from a Fable planning pass

Followed v1.8.451's bug-fix batch with a Fable-model planning pass asking one question: is Ask AI actually serving its real job — a fast-paced, ADHD-affected inspection workflow — as well as it could? Verdict: close to right-sized, no case for a big new capability, but five concrete, ranked improvements. All five built this pass.

1. Grade Wizard "Ask AI instead." A fault search with no match had no path to Ask AI at all — the exact moment the feature's own prompt design targets (a fault in hand, nothing documented), and the app's own unmatched-search log proves happens routinely. The Phrases sidebar search got this escape hatch in v1.8.390; the higher-stakes Grade Wizard never did. Closing the modal first (same "close before navigating" pattern as v1.8.451's "Manage notes" fix) and handing off to the same pre-fill-only jump every other Ask AI entry point uses.

2. Prompt caching on the knowledge base. The ~40-60k char knowledge base was sent fresh on every single call, including every follow-up turn of a thread where it's byte-identical — the real driver behind the multi-second waits that made the "Thinking…" placeholder necessary in the first place. Split the per-question camera-DB match into its own uncached block so it can vary freely without invalidating the cache of the large, session-stable instructions+KB block ahead of it (Anthropic prompt caching, ephemeral/~5min TTL — comfortably covers a real back-and-forth).

3. One-click copy on the phrase, not just the grade. The GRADE sentinel already became a copyable chip; the exact approved phrase — the other thing the prompt explicitly asks for — had no equivalent, so the terminal action of most grading questions (pasting exact wording into NOVA) meant trimming it out of the prose by hand. New PHRASE sentinel, parsed the same way as GRADE/FEEDBACK_NOTE, rendered as its own chip with a copy button.

4. AI-triage the unmatched-search log. Every logged no-match search (Settings → Data → Unmatched searches) cost the same manager-communication path, whether it was a genuine documentation gap or just a vocabulary mismatch. New "Triage with AI" button (new triageSearchGaps Cloud Function) splits the list into "actually covered under different wording — fix it yourself with a note, right here" vs "genuine gap — still worth raising," with a one-click "Add note & remove from list" per covered entry.

5. See the knowledge base as the AI sees it. Guide section 10 showed only a character count — when an answer was wrong, there was no way to check whether the AI was missing the fact, had it tagged UNVERIFIED, or had a contradicting note, without spending another paid question. Added a collapsed, scrollable raw view of the exact payload every call sends.

Also folded in as a small related fix: the per-item "Ask AI about this item" button's canned question ("What should I know about inspecting/grading X?") was generic enough to spend a real call on a low-value answer if sent unedited — now pre-fills an open stem (the item name, ready to keep typing the actual fault) instead.

v1.8.451

Ask AI: fixed a real data-loss bug, plus a first-question loading fix and a "paste the whole manual" pass

Three Fable-model QA passes ran back to back against the live Ask AI sidebar tab, the sidebar search/UX generally, and mobile layout (Grind tab excluded). The most serious finding: the owner's OWN live multi-tab settings sync listener blindly replaced customDeclineReasons/aiKnowledgeNotes wholesale on every cross-device settings write — the exact same failure shape the v1.8.343/v1.8.360 boot-time merge was already patched for, just never carried over to this listener. Reproduced for real: a note saved with the "Saved" confirmation showing vanished the instant another device's settings write echoed back. Now unions both arrays by id/text, same as the boot-time merge.

Also fixed, all from the same QA batch: the first question in a new Ask AI thread never rendered as a "You:" turn and the panel showed nothing but a tiny "0s" timer for the whole round-trip (now echoes the question immediately with a "Thinking…" placeholder that fills in when the answer lands); lastQuestion — the context "Add a note" attaches to a correction — was a local variable that silently reset to empty every time the sidebar was closed/reopened, so a note could save as Re: "" — ... with no question attached (promoted to a persistent global, same pattern already used for the running conversation); the note form's stale "Saved" confirmation used to survive "New question", a sidebar close/reopen, and even deleting the note itself; "Manage notes" left the sidebar open over the very Guide section it navigated to and often landed at the top instead of scrolling to it; and AI answers rendered literal **asterisks** instead of bold.

Separately: Guide's "Check for manual updates" section actively talked you out of pasting a whole updated manual in ("works best on a specific section") even though the server-side cap was already raised in v1.8.381 specifically to support exactly that. Copy rewritten, textarea grown, and a live character counter added against the real 100,000-char server limit — plus a client-side pre-flight check so a too-long paste fails instantly instead of after a round-trip.

v1.8.450

Fixed a real bug: the quote parser was skipping every single line

Reported live: pasting a fresh allocation produced "0 items · 0 quotes" — confirmed by re-pasting the exact reported text and watching parseInput skip all 10 lines. Root cause: the Wex export gained an extra column between duration and date that wasn't there before (either blank, or an "xN" column meaning how many parcels that quote spans — unrelated to any one item's own inspection time). parseInput only ever stripped a single trailing column, so on any row with the extra column it grabbed that blank/"xN" text as the duration string instead of the real one, which parses to 0 minutes and gets silently skipped. Fixed by locating the duration column by CONTENT (a colon/h/m-shaped token, searched right-to-left) instead of assuming it's always the last column — everything after it, whatever it is, is now discarded as noise. Falls back to the old last-column behavior only when no duration-shaped token is found at all, so unusual bare-number formats still work as before.

v1.8.449

NOVA offline promoted to a full tab; tab bars now scroll on mobile

Real feedback, same day: "nova offline needs to be a full page thing, also the tabs are off the screen, move to a top bar on mobile." NOVA offline moved out of the sidebar drawer entirely — no more rail icon or sidebar tab — and is now its own full-width tab (next to Audit) with a real card grid instead of a stacked list cramped into a ~320px column; a badge on the tab shows how many un-transcribed entries are waiting, hidden at zero, same convention as the Audit badge. Separately, and the actual root cause of "the tabs are off the screen": both the main tab bar and the sidebar's internal tab strip were a plain flex row with no wrap and no scroll — with 8 main tabs now (NOVA offline included) there's no font size small enough to reliably fit a 360-390px phone, so tabs past whatever happened to fit were simply unreachable. Both strips are now real horizontally-scrollable top bars on mobile (swipe to reach the rest), the same pattern Settings' own internal tabs already used.

v1.8.448

Grind toasts removed; a mobile pass on NOVA offline

Two follow-ups on real feedback. Grind's own toast stream (level-ups, buffs, trades, Slayer/GE activity — separate from the app-wide toast) is gone: showGameToast is now a no-op rather than stripped from its ~90 call sites across grind-core.js/grind-view.js, so nothing changes behaviorally except that nothing pops up any more. Also a mobile pass on yesterday's NOVA offline pane, since it shipped with zero live-device testing: the 5th sidebar tab now reads "NOVA" instead of "NOVA offline" so a 5-tab strip doesn't wrap on a phone-width drawer, the entry-form modal's body no longer double-caps its own scroll height against the framework's existing one, and every checkbox row (Grey import, Missing, Repair required, Declined, the two "not required" toggles) is now a full-width tappable label with real padding instead of relying on the native checkbox's tiny hit box.

v1.8.447

NOVA offline — fill in NOVA's fields on-device when NOVA (or wifi) is down

Built live, mid-outage — work wifi dropped and nova.wexphotovideo.com was unreachable but inspection still had to carry on. New "NOVA offline" sidebar pane (rail icon next to Ask AI) captures the same fields NOVA's own inspection form asks for — item/QID/SKU, serial number, category, current location, grade, sensor quality, dust level, status, shutter count and firmware version (each with its own "not required" toggle, matching NOVA's real checkboxes), grey import, missing (+ which accessories), repair required, declined, inspection/retail cost, and notes — entirely on-device, no network needed. Each item's right-click menu gained a "Fill offline NOVA…" entry that opens the form pre-filled with that item's name/QID/SKU. Entries save to a running list with Copy / Edit / Delete per entry plus a Copy-all, formatted as plain paste-ready text — the idea is to fill it in now and manually transcribe into real NOVA once it's back, then delete the entry. Deliberately local-only (not synced to Firestore) — this is bridge scratch data, not permanent app state, and it can carry serials/notes that have no reason to leave the device.

v1.8.446

Nikon shutter reader: fixed a real big-endian byte-order bug

The v1.8.445 debug output found it: a genuine Nikon NEF came back with first bytes "4D 4D 00 2A" — "MM", the BIG-ENDIAN TIFF signature. The reader was built by reusing the Sony reader's tag-decoding helpers, which hard-code little-endian byte math throughout (correct for Sony — every real Sony file is "II" — but never actually endian-aware). Nikon commonly writes both its embedded JPEG EXIF and its NEF files as big-endian, the opposite default from Sony, which is also why the JPEG test in the same round came back with an empty make/model instead of a real camera name. Rewritten with its own small endian-aware tag reader (detects "II" vs "MM" per file, including the MakerNote's own separately-marked nested TIFF header) rather than making the shared Sony helpers endian-aware and risking the verified-working Sony path. Debug output stays on for one more round of real testing before it's removed.

v1.8.445

Nikon shutter reader: temporary debug output on a real failure

Real testing against a genuine straight-off-camera file hit "Doesn't look like a JPEG or Nikon raw (.NEF) file" — a check that should be impossible to fail on an unmodified file, since it's just looking for the JPEG/TIFF file-format signature every real camera file has. Rather than guess at a fix blind, the error now temporarily also shows the file's actual type/size and the real first 8 bytes read, so the next attempt pins down whether the bytes being read are really wrong (a genuine bug) or something else is going on. Remove this debug tail once a real file has been confirmed working end to end.

v1.8.444

Grading: "Read count from photo" now also works for Nikon

Same no-upload, no-network client-side EXIF parsing as the existing Sony shutter-count reader, added for Nikon's own MakerNote layout (JPEG or .NEF) — sits right next to the Sony button in Grading's shutter/sensor panel, feeding the same count field either way. Nikon's ShutterCount tag (0x00A7) is a plain, unenciphered value once you're inside its MakerNote's own nested TIFF header, so this reader is structurally simpler than the Sony one (no per-model address table, no substitution cipher) — the one real gotcha is that MakerNote's offsets are relative to its own nested header, not the outer file's, which the code accounts for explicitly. Not yet verified against a real Nikon file — the Sony reader only reached its current working shape after a real test caught real bugs, so treat a reported count as unconfirmed until it's been checked against an actual Nikon JPEG/NEF.

v1.8.443

"Ask AI about this item" button on every allocated item

A new icon button on each item row (an 11th, alongside camera info/grading/sub/remove — a deliberate call given the row was already crowded) jumps straight to the sidebar Chat tab with a question pre-filled naming that exact item, same pre-fill-only-never-auto-send behaviour as the Phrases tab's own "Ask AI instead" hint (both now share one openAiChatWithQuestion helper). No new context is sent — Ask AI's existing knowledge base already covers grading/handling/accessories for whatever the item turns out to be.

v1.8.442

Guide: Panasonic lens code lookup table, fed into Ask AI

New Guide section (07, between Elucid and Writing Inspection Notes) transcribing a Wex reference sheet for identifying Panasonic lenses by their printed code — several codes are genuinely reused across different mark I/II lenses, most heavily in the 14-42mm f3.5-5.6 family, and those rows are flagged in red as a visual-check reminder rather than a code-only lookup. Each row has a "Panasonic" link (searches store.panasonic.co.uk for that exact code — a real search URL, not a guessed product page) and a "Find SKU" button that fuzzy-matches the lens against this app's own synced Product Lookup catalogue and links straight to the matching SKU in NOVA. The whole table is also fed into Ask AI's knowledge base (PANASONIC_LENS_CODES), so a lens-code question can be asked there directly, same as the existing battery-replacement table.

v1.8.441

Grind: fixed a real duplicate-vertex bug in large corner drags

Caught in direct testing before shipping any further: dragging a corner a large distance could produce a shape with two different points at the exact same tile — a candidate position's own landing spot was checked against the rest of the shape, but the straight-line RUN needed to actually reach it (from one neighbor, through it, to the other) wasn't, so it could clip through some other unrelated part of a complex shape along the way. Fixed at the source (candidate generation now checks the whole run, not just its endpoint) plus a defensive re-check at commit time.

v1.8.440

Grind: "Reset map" now warns it deletes hand-drawn buildings too

Real gap, caught live: the confirm dialog only mentioned resetting dragged node/building positions — it didn't warn that Reset map also permanently deletes every hand-drawn custom building. Now says so explicitly (with a live count) whenever at least one exists.

v1.8.439

Grind: real building-workflow rework — straight-line drawing, drag-to-resize corners, a rectangle tool

Real request, direct: "the vertex placement system is clunky and needs working on." Every click used to have to land on a grid point one tile-step from the last — tracing a long wall meant one click per tile. A click now just needs to be aligned (horizontal, vertical, or a real 45° diagonal) with the last point at ANY distance — the whole straight run of intermediate tile-corners auto-fills in one click, so a rectangle is 4 clicks (its corners), not one click per tile of wall.

Reshaping a corner (drag any building's vertex handle) had the same limit — only the 8 tiles immediately touching its old position were ever valid, so resizing meant many small repeated drags. A corner can now drag to any position aligned with both its neighbors, at real distance — dropping it splices in the real straight-line run needed to get there (not just relocating one point), and any door on the wall that moved gets re-found by its actual endpoints in the new shape, or dropped with a toast if that specific wall no longer exists rather than silently misplaced.

New: drag (not click) with the Draw tool active, before starting a trace, to draw a rectangle in one gesture — most buildings people actually draw are plain rectangles, and this skips the click-by-click tracer entirely for that common case.

v1.8.438

Grind: fixed the map collapsing to a ~200px strip after the sidebar became a real column

Real bug, caught live right after v1.8.437 shipped. .farm-grid-viewport has a fixed height:200px in its base rule, left over from before the full-viewport redesign — while the map/sidebar row was flex-direction:column, flex-grow correctly grew past that on the height axis without issue. The moment it became flex-direction:row (for the side-by-side sidebar), flex-grow started governing width instead, and height — now the cross-axis — went back to respecting that old fixed 200px literally. Fixed with an explicit height:100% override.

v1.8.437

Grind: the sidebar is a real full-height slide-out panel now, not a floating box

Real request, direct: "now we have a bigger surface for the viewport move the sidebar into a slide out sidebar in the viewport full height, that sits inline with the viewport." The Bots/Skills/Slayer/Trade/Hiscores panel was an absolutely-positioned floating HUD box (300×400px, translucent, blurred) — a design from back when the tab couldn't spare real layout space beside the map. The near-fullscreen modal removes that constraint: the panel is a genuine flex sibling of the map now (same row, full modal height), collapsing by sliding its width to 0 instead of just floating out of the way — collapsing it actually gives that space back to the map instead of hiding a box that was never taking any layout room to begin with. Solid background matching the shell's own established colors now too, since the translucent-blur treatment specifically justified itself by "the map shows through behind it," which stopped being true the moment it stopped overlapping the map.

v1.8.436

Grind: the tool palette and the button row are actually inline now

Real complaint, direct, with a screenshot: "im actually so suprised you cant figure out to put all the buttons inline, you keep stacking them." Fair — the v1.8.432/433 passes merged the tool palette and the button row into one shared background block, but the palette was still its own centered row underneath, not genuinely on the same line. It's one real flex row now: tool icons + hint on the left, How this works/Edit map/Reset map/Debug paths pushed right — no wrapping, no second line, the modal's plenty wide enough for both halves at once.

v1.8.435

Grind: fixed the map rendering in a tiny box inside the new modal

Real bug, caught live right after v1.8.434 shipped: the map rendered in a small ~200px box with a large dead area below it instead of filling the modal. Root cause: #grindContent (renderGrindView's own mount point) sits between the modal body and the game shell with no sizing of its own — a plain div shrink-wraps to its content instead of passing "fill available height" down, breaking a flex chain that worked correctly everywhere else around it. Fixed.

v1.8.434

Grind: converted from a tab to a modal, near-fullscreen

Real request, direct: "can we move the grind tab into a modal?" Grind is opened from the same button in the same spot in the tab bar, but no longer occupies a permanent tab slot — it opens as a near-fullscreen modal instead (96vw × 92vh, not one of the app's usual small-to-medium modal sizes — the isometric map genuinely needs the room). Closing it doesn't lose anything: map pan/zoom, the selected sidebar panel, in-flight bot animations, edit-mode state, all survive exactly like switching away from a tab and back always did — the modal just hides, it doesn't tear anything down. Escape/backdrop-click/the close button all work via the same accessibility system every other modal in the app already gets for free.

v1.8.433

Grind: the top bar actually matches the shell's own look now

Real follow-up, direct: "all you did was move the section up to the top it doesnt look like part of the same ui." Fair — v1.8.432 fixed the layout (one block, no gap) but kept a palette invented just for that bar (near-black background, bright yellow throughout) instead of the shell's own established colors, which every other in-shell strip (the sidebar's own tab row, the NPC dialogue box) already uses consistently. Retextured to match exactly — same background, same border, same hover/active treatment (a solid fill + amber underline glow, not a translucent bordered square) — so it now reads as genuinely part of the shell instead of a different UI bolted on top of it.

v1.8.432

Grind: the button row and the tool palette are one cohesive top bar now

Real request, direct, with a screenshot: "make the flaoting bit and the top buttons one complete top bar that spaces well with no padding around the viewport, and improve and make it cohesive." The tool palette (added v1.8.430) was a separate box floating on top of the map, absolutely positioned over whatever building happened to be underneath it, with a visible gap above between it and the button row. It's a real second row of the SAME bar now — one background, one border, flush against the map with no dead space between them — the palette only appears (as a thin second strip) while Edit Map is actually on.

v1.8.431

Grind: tutorial text catches up to v1.8.430's new tool palette

The "How this works" tutorial's Edit map section still described the old Shift+click/separate Draw Building button setup from before v1.8.430's unification. Rewritten to describe the real four-tool palette (Move/Block tile/Block edge/Draw building), the vertex-drag ghost preview, doors-via-wall-click, and that presets reshape exactly like a hand-drawn building now.

v1.8.430

Grind: preset buildings unified with hand-drawn ones, a real tool palette, NPCs freeze too, doors are actually visible

Real request, direct: "custom building and preset building should be exactly the same." Castle, Bank, Rune shop, Windmill, and any built Construction structure are no longer a separate hardcoded-rectangle system — they're seeded as ordinary hand-drawn-style shapes (the same vertex-loop representation, generated once from their old default footprint) and rendered/collided/edited through the exact same code hand-drawn buildings already had. Every editing tool built for custom shapes now works on the core buildings too: drag a corner to reshape the bank, click a wall to add a door to the castle, drag the whole roof to move the rune shop. A core building still can't be deleted by accident (right-click on its corners offers to reshape, not remove) — everything else about it is unguarded. Existing saved layouts and hand-drawn buildings migrate automatically; nothing was lost.

Real request, direct: "the toolbar should be like photoshop you click the tool not jsut a wall of text." The box above the map used to be a single paragraph swapping between two long sentences. It's a real tool palette now — four icon buttons (Move, Block tile, Block edge, Draw building), exactly one active at a time, with a one-line hint for whichever tool is selected instead of a wall of text describing all of them at once. Block tile/Block edge replace the old plain-click/Shift+click convention with an explicit pick; dragging and door-toggling are both part of Move.

Two more real gaps from the same feedback: dragging an existing building corner to reshape it now shows a ghost at whichever position it'll actually snap to on release (only the drawing tool had this before); and NPCs (Turael, Aubury, the Banker) now stop wandering while Edit Map is on, matching how bots already freeze — they just don't disappear too, since they're still valid drag targets while editing.

Real complaint, direct: "theres no visaul way to see a dor unless it isnt setting a door" — the door graphic (a subtle dark inset on the wall's own color) could get lost against certain wall tones, made worse the moment preset buildings gained their own themed colors in this same update. Replaced with a distinct wood-plank pattern that always wins regardless of which building it's on.

v1.8.429

Grind: fixed points/doors going unresponsive after drawing, bots freeze off-screen in edit mode, right-click/scroll contained to the game

Real bug, direct report: "i cant seem to move points after placing" and, separately, "click to add a door doesnt work." Both traced to the same cause — the v1.8.427 fix that made Draw Building an exclusive tool (nothing else responds to a click while it's tracing a new shape) never actually got switched back OFF once a shape was closed, so it silently stayed the active tool and kept blocking every other Edit Map interaction on the building you'd just finished drawing. Fixed by deactivating Draw Building the moment a trace closes, whether you go on to save it or cancel the name.

Real design idea, direct: "in edit mode it freezes the simulation, hides the bots, and if they are in an invalid point it teleports them to the nearest available point." All three, built as asked — bots stop animating and disappear from view the moment Edit Map turns on (purely the cosmetic layer; the underlying elapsed-time economy is untouched), and any bot whose current tile a collision edit just invalidated relocates to the nearest open tile via the same pathfinding step-set _findPath already uses, silently, before it's ever visible again.

Also: right-click anywhere in the Grind game now only ever opens the app's own menus, never the browser's native one — previously only individual entities (bots, buildings, NPCs) suppressed it, leaving empty ground and sidebar space to fall through to the OS menu. And scrolling anywhere in the Grind tab (not just directly over the map) no longer scrolls the page underneath it, except where it should — the sidebar's own bot/skill lists still scroll normally.

v1.8.428

Grind: fixed a real data-corruption bug, added an edit-mode toolbar, furniture now moves with its building

Real bug, direct report: "letting go of a point puts a manual tile blocker down." Traced to the shared drag primitive every draggable entity in Edit Map uses: releasing a drag called stopPropagation() nowhere, so the pointer-up event kept bubbling to the map viewport's own click handler underneath — which, finding no drag of its own in progress, treated the release as a genuine click on bare ground and fired whatever Edit Map's plain-click tool currently does (toggle a tile, or place a new trace point). This fired on every drag release of every draggable thing this function has ever wired — nodes, buildings, NPCs — not just the new vertex handles; it just took a workflow that drags things constantly to make it obvious. Fixed by mirroring the exact stopPropagation() the drag's own pointer-DOWN already had, on pointer-UP too.

Also: a persistent toolbar now shows at the top of the map whenever Edit Map is on, listing what every current gesture actually does — swaps entirely to Draw Building's own gesture list while that tool's active. And furniture (the castle's throne, the bank's counter, etc.) now moves along with its building when dragged, instead of being left behind at its old absolute position — a real gap that turned out to have never been persisted across reloads either, now fixed at both ends.

v1.8.427

Grind: whole-building move, undo, self-intersection guard, and Draw Building goes exclusive

Four real gaps closed after "improve building builder": a closed shape can now be moved as a whole (drag its roof) instead of dragging every corner individually; Backspace undoes just the last placed point mid-trace instead of scrapping the whole thing (Esc still does that); the shape outline can no longer be drawn or reshaped into something that crosses itself, checked both while placing each new point and as a filter on every valid vertex-drag position; and interior-tile detection is now cached per building (invalidated automatically the moment its vertices actually change) instead of re-running the full ray-cast from scratch on every occupancy tick.

Also fixed a real friction point, direct: "differentiate a tool between moving points and creating new ones." Draw Building and every other Edit Map tool (vertex dragging, whole-building dragging, door toggling) were both live at once whenever edit mode was on, so a click meant to place a new point could get intercepted by an existing building's vertex handle instead. Draw Building is now a genuinely exclusive tool — while it's on, nothing else responds to a click, and existing handles/walls visibly dim to show it.

v1.8.426

Grind: custom buildings get real interiors, doors, and a drawing ghost preview

Follow-up on the Draw Building tool, direct: a ghost preview of where the next point will land (and whether it's valid), a real walkable interior once a shape's built, and doors. Confirmed scope before building: real interior (floor + roof/wall fade when a bot's inside, same as the castle/bank) rather than just a drawing-time cosmetic, and doors toggle by clicking a wall segment AFTER the shape is closed rather than being marked mid-draw.

Interior detection needed real geometry a rectangle building never has to do — there's no "bounds minus the wall ring" shortcut for an arbitrary polygon, so this is standard even-odd ray-casting against the vertex loop (works identically for orthogonal and diagonal edges; the algorithm only cares about vertex positions). A door is just a segment INDEX excluded from that shape's generated collision — the exact same "excluded tile" idea the rectangle buildings' doorTiles already uses, just indexed by wall segment instead of by tile, so it needed zero new collision machinery, verified live by pathing a bot straight through a door segment into a previously-sealed interior. Floor rendering reuses the roof's own clip-path (per the interior-detection point, the whole polygon is walkable now, so there's no separate floor shape to derive) and the occupancy fade reuses the existing .iso-building-occupied CSS untouched, since custom building walls already carry the same base classes the rectangle buildings' fade rule targets.

The ghost preview hooks into the exact validity check the hover readout already computed (queen-step from the last point / closes the loop / duplicate point) rather than recomputing it a second time — a color-coded ring at the candidate grid point plus a line back to the last placed point: green for a valid next click, gold when hovering the start point closes the shape, red when the click would be rejected.

v1.8.425

Fixed real bug: every icon app-wide broke, caused by the previous release

Direct report: "the icons are broken." Real regression, app-wide, not scoped to Grind — adding architecture to the Material Symbols icon allowlist (index.html, for the new Draw Building button) put it in the wrong position: architecture sorts BEFORE archive alphabetically (the 6th letter, t vs v, decides it), but it was added after. Google Fonts' icon-subsetting API requires the name list sorted exactly alphabetically and rejects the WHOLE request with a 400 if it isn't — so instead of degrading gracefully, every single icon on every page silently fell back to plain text (e.g. "help HOW THIS WORKS" instead of a help icon) app-wide, the instant this shipped. Caught it live by direct navigation to the actual font-CSS URL, comparing the broken and fixed versions side by side, and confirmed via a full alphabetical-sort check across the entire icon list before shipping the fix — not just the one name that moved.

v1.8.424

Grind: draw your own building shapes, straight onto the grid — diagonals included

Real feature request, direct: "support drawing along edges of the grid to create building shapes, add diagonals in too." A new "Draw building" tool inside Edit Map (Grid → Edit map → Draw building): click grid points to trace an outline — each click must land on a point adjacent to the last one, orthogonally or diagonally (the real OSRS diagonal-wall mechanic, a wall cutting across one tile's corner instead of running along its side) — then click back on the start point to close the loop and name it. v1 is a solid obstacle, same as the Windmill: no door, no floor, no walkable interior. An orthogonal segment reuses the exact edge-collision system the last two versions built (the wall-ring stays occupiable, only crossing it is blocked); a diagonal segment blocks the one tile it visually cuts across, since — worked through properly rather than guessed at — there's no clean "two tiles this separates" the way an orthogonal edge has, only one tile it's the corner-to-corner diagonal of.

Once a shape's closed, every corner gets its own draggable handle for reshaping in place (Danny's own call over delete-and-redraw) — a vertex can only be dragged to a position that keeps both its neighboring walls valid single-tile steps, computed as a small candidate set up front rather than validated after the fact, so a shape can never end up broken mid-edit. Right-click a handle to delete the whole shape. Rendering reuses the existing wall-segment clip-path primitive (now factored out standalone) and generalizes the rectangle buildings' hardcoded "south+east walls only" visibility rule to a real per-edge outward-normal test — verified by hand to reduce to the exact same south/east rule for a plain rectangle, confirming it's a true generalization and not a different rule that happens to also work.

v1.8.423

Grind: buildings rebuilt ground-up, plus a real diagonal-cutting bug from edge collision

Two real, direct fixes. First: a diagonal-movement bug from the previous version's edge collision, caught by testing live before shipping — converting building walls from whole-tile to edge blocking meant wall-ring tiles became walkable, so the old corner-cutting guard (which only rejected a diagonal step when a flanking TILE was blocked) no longer caught a diagonal that grazed straight past a building's outer corner, since neither flanking tile was blocked anymore, only specific edges. Confirmed: a path from inside the castle to just outside cut through the SE corner in two diagonal hops, walking clean through solid wall instead of detouring to the door. Fixed with `_diagonalCrossesBlockedEdge`, checking all 4 real edges of the "corner square" a diagonal step passes through, not just the two flanking tiles.

Second: buildings were being constructed roof-down instead of ground-up, direct correction: "we build the buildings from ground up not from roof down." The roof used to sit AT the tile grid's true ground position with walls hanging BELOW it by wallHeightPx to fake height — meaning a wall's actual foot (where it touches the ground, where the collision debug layer's markers correctly render) sat wallHeightPx below where the wall visually appeared to end, making collision look like it hugged the ROOFLINE instead of the wall's base. Fixed by raising the whole building's origin by wallHeightPx instead — the wall's foot now lands exactly on the true, unshifted ground position, and the roof sits above it. One resolved side effect: a building's floor and a bot standing inside it are now both at true ground level, closing a small permanent visual gap between a bot's feet and the floor that existed since the building renderer was first built isometric.

v1.8.422

Grind: buildings walk on their own walls now, and the edit-mode selection box actually hugs the building

Follow-up on the previous version's edge-collision system, confirmed direct: "both" — make automatic building walls use real edge collision instead of whole-tile blocking, and fix the Edit Map selection outline. Every enterable building (castle, bank, rune shop, any built Construction structure) now generates its wall collision as EDGES instead of whole blocked tiles: `_buildingWallEdges` walks the same wall-ring tiles as before but blocks only each one's boundary facing outside the building, so "the inside of the wall can still be occupied" — a bot can now genuinely stand on the wall-ring tile itself, only actually crossing through the wall is blocked. `BLOCKED_EDGES` picked up the same manual/automatic split `BLOCKED_TILES` already had (`MANUAL_BLOCKED_EDGES`/`MANUAL_UNBLOCKED_EDGES`, merged back in on every `rebuildMapDerivedData()`) so an Edit Map edge toggle survives a building drag or a Construction build finishing instead of getting silently wiped. The river and the (door-less, fully solid) Windmill are unchanged — whole-tile blocking is still correct for both.

Second real bug, caught live in a screenshot: the Edit Map dashed "you can drag this" outline around a building (`.iso-building::after`) was a plain CSS rectangle around the element's own bounding box — and an isometric building's bounding box is much bigger than its actual diamond-plus-walls silhouette, so the dashed box floated well past the castle's real edges, overlapping unrelated terrain nodes. Replaced with a real SVG polygon traced from the exact same corner math the building's own roof/walls already use (`_isoBuildingSilhouettePoints`) — the outline now hugs the true visible shape exactly, roof diamond and wall base included. Terrain nodes keep their existing CSS-circle outline; that one was never the problem.

v1.8.421

Grind: real edge collision — block one side of a tile, not the whole tile

Real feature request, direct: "how do we block a wall (one side of the tile) instead of the whole tile so the inside of the wall can still be occupied." `BLOCKED_EDGES` is a genuine second collision type alongside whole-tile `BLOCKED_TILES` — a flat set of canonical tile-pair keys blocking the boundary between two specific orthogonally-adjacent tiles while leaving both tiles themselves fully walkable. In Edit Map: plain click still toggles a whole tile; Shift+click toggles just the nearest edge (a purple line along the real shared tile boundary, shown live in the hover readout before you commit so you know exactly which edge a click will hit). `_findPath`/`_maybeWander` both check it for orthogonal steps — diagonal movement has no single shared edge to cross (it passes through a corner where 4 tiles meet), so it's untouched, still governed by the existing corner-cutting rule. Currently manual-only (Edit Map's own tool); nothing automatic generates an edge block yet — the source-of-truth reference block (grind-view.js) is updated to describe both collision types and when each applies.

v1.8.420

Fixed the diamond-highlight bug, Debug Paths lag, and wrote a canonical grid/collision reference

Two real bugs from the previous version's own debug tooling, both reported live within moments of shipping it. The tile "diamond" highlights were rendering as plain rectangles: the corner formula shifted only one of tx/ty per corner instead of both together (unlike every other proven-correct footprint calc in this file), which lands at a small axis-aligned box instead of the tile's real diamond boundary — fixed to match the same pattern `_isoFootprintBox` already used correctly. Debug Paths also visibly lagged the map: the whole overlay (every blocked tile's diamond, 50-100+ across the map) was being rebuilt from scratch on every 600ms bot tick even though blocked tiles essentially never change tick to tick — split into two separate SVG layers, a static one for blocked tiles (redrawn only when collision genuinely changes, hooked directly into the one function that ever changes it) and a dynamic one for bot true-tiles/paths/pins (redrawn every tick, same as before, since that part actually needs to be).

Also wrote a real "source of truth" reference block directly into the code (grind-view.js, right at the tile-grid constants) after a direct request to stop re-litigating the same coordinate-system confusion release after release — covers the coordinate system, why there's no separate "ground" reference frame, why wallHeightPx must be a multiple of the tile height, why collision is deliberately whole-tile-only (audited: no current building needs true edge-collision), the corner-cutting rule, and a labelled diagram of the wall-face-naming convention that caused v1.8.339's bug. Reviewed by an independent higher-tier model pass before shipping, checking every claim against the real code rather than trusting it read plausibly — which caught a genuine bug in the doc's own subject matter: `_maybeWander`'s corner-cutting check (added last version) computes its candidate tile relative to the bot's fixed home anchor, not its live position, so it could compute a 2-tile diagonal jump the flanking-tile test doesn't actually validate for. Fixed by skipping any wander roll that isn't a genuine single-tile hop from the bot's real current position.

v1.8.419

Fixed the real pathing bug: corner-cutting, plus real tile-shaped debug tools

Pinpointed the actual bug behind "bots walk through walls": distinguishing whole-tile collision (a tile you simply can't stand on, e.g. the river) from wall collision (an edge you can't cross, even while standing on the tile it runs along) surfaced it directly. Live data showed multiple bots taking the diagonal step (14,6)→(13,7) right past the castle's real corner wall tile (13,6) — legal under the old corner-cutting rule (only rejected a diagonal when both flanking tiles were blocked), but a wall is a real plane occupying that tile's edges, so cutting diagonally past even one blocked corner still crosses through part of it. Tightened `_findPath`'s corner-cutting guard (and `_maybeWander`'s idle fidget, which had no corner check at all) to reject a diagonal step if either flanking tile is blocked — a bot now goes around a corner via two orthogonal steps instead of one diagonal graze past it. Also audited every other placement/collision system in the game (decorative props, interior furniture, buildings, nodes, bots, NPCs) — all already derive position purely from tx,ty via the same grid math, nothing else needed fixing.

Debug tools, real feedback throughout: blocked tiles, a bot's "true tile" (its actual discrete simulation position, distinct from the sprite's own mid-glide animated position), and pinned tiles now all highlight the ACTUAL isometric diamond shape of the tile instead of an abstract dot or box — no more separate "which height does this marker sit at" question to ask at all. Added coordinate labels on true-tile and pinned markers, a live hover readout showing the tile under the cursor, and click-to-pin (while Debug Paths is on, Edit Map off) to mark an exact tile and report a collision issue precisely instead of describing it visually.

v1.8.418

Fixed real bug: debug dots and bot paths were in different coordinate spaces

Pushed back on directly, correctly: "the green lines are clearly crossing over the red dots." They were — visually. Root cause: v1.8.416 shifted blocked-tile dots down by `wallHeightPx` to match a wall's visual facade, but bot path lines stayed at plain, unshifted ground level (the same height bot sprites themselves always render at). A bot walking just outside a building's wall renders at one screen height; that wall's dot, shifted down 24-48px to match its facade, could land at nearly the same screen height despite being on a completely different tile row — a real visual crossing with no logical one behind it (confirmed by checking every waypoint of every moving bot's actual route against `BLOCKED_TILES` directly — zero matches, both before and after this fix). For a tool whose entire job is "does this path avoid these tiles," dots and paths have to share one reference frame so a visual match always means a real match. Reverted dots to plain ground level — they'll look like they're "on the roof" again for tiles inside a building's own footprint, which is now understood to be correct rather than a bug: the collision grid itself is flat, and a tile under a roof is exactly as invisible to a dot as it is to a bot standing there before the wall fades. Also added: a "true tile" marker (cyan square) per bot, showing the actual discrete tile the simulation has it on right now, separate from the sprite's own on-screen position (usually mid-glide between two tiles thanks to the movement animation) — deduped per tile, same "stacking" bots already do when several share one, so a crowded node doesn't carpet the screen in overlapping markers.

v1.8.417

Grind: Edit Map can now add/remove collision directly

Real feature request: click any empty ground tile while Edit Map is on to toggle it blocked/walkable, no dragging needed. Manual edits (`MANUAL_BLOCKED_TILES`/`MANUAL_UNBLOCKED_TILES`) are kept separate from the automatic collision set (buildings/river) since that set is fully recomputed from scratch on every node/building drag — a manual toggle written directly into it would just get wiped by the next unrelated edit; the two are merged back in every time instead, so they survive. Blocked-tile dots (from the Debug Paths overlay) now also show during Edit Map itself, colour-coded — orange for a manual edit, red for real building/river collision — so you can see exactly what a click is about to change before making it. Closes a real gap flagged in the same conversation: a bot's route is computed once and only consumed tile-by-tile after that, never re-checked against a route already in progress — every currently-moving bot now re-paths immediately after any collision toggle, using the same real pathfinding its original route used, instead of potentially finishing out a route that just became invalid.

v1.8.416

Fixed real bug: debug-paths dots sat on roofs instead of ground level

Reported live immediately after shipping the debug overlay, using it for real for the first time with actual bots. Blocked-tile dots were drawn at plain `isoToScreen(tx,ty)` — the same un-shifted position the roof itself uses — but a building's walls (and the real ground they stand on) sit `wallHeightPx` below that, the same offset the floor plane already uses. Since the overlay renders at a very high z-index specifically so it's never hidden, a dot left at roof height rendered right on top of the opaque roof surface instead of down at the wall's actual base, reading as "dots covering the roof" rather than marking the real collision. Fixed by reusing `_buildingAtTile` (the existing shared "which building owns this tile" check) to look up the right building and apply the same height offset its own walls use — the Windmill needs an explicit check since it's deliberately not in the building registry (not enterable). Tiles that aren't part of any building at all (the river) get no offset, since there's no wall there and ground level was already correct.

v1.8.415

Grind: grid on interior floors, and a real debug-paths overlay

Two pieces of real feedback. Floor grid: a revealed building interior had no grid, unlike outdoors — the outdoor diamond overlay sits at z-index:0, well under the floor's own z-index:500, so an opaque floor plane fully hid it. Rather than raising the global overlay's z-index (which would also start drawing it over ground-level props/nodes everywhere else on the map), added a second small set of grid lines scoped to just each building's own floor footprint, one step above the floor. Debug paths: a new toggle (toolbar, next to Edit Map) for "bots seem to just wander through walls and over buildings" — draws every currently blocked tile as a red dot, each moving bot's actual precomputed route (read directly from the same array the real movement code consumes, never a separate recomputation that could itself drift from what's really happening) as a green dashed line, and a ring around its real destination tile. Lets you see directly whether something's a genuine collision gap (a bot's route crossing a red dot) or a depth-sorting issue (the route is clean, but a wall visually occludes wrong) — not yet verified against a live bot, since testing coincided with a farm that currently has none; the tool itself is exactly what's needed to check once one exists.

v1.8.414

Fixed real bug: the Add Item input kept disappearing mid-type

Reported live, actively blocking real work. Root cause: the Add Item form is injected as plain, imperative DOM — it has no representation in `state` at all until actually submitted, so a full page redraw has no idea it's open and simply doesn't include it. Several Firestore sync listeners (state, settings, actuals, xp, farm — anything that can change from another tab or device) called that full redraw unconditionally whenever a remote update landed, with no awareness a form was open mid-edit — silently discarding whatever had been typed or pasted. Two of these (state, settings) were directly observed firing live during this exact investigation. Fixed by tracking which quote (if any) currently has the form open, and skipping the redraw — not the underlying data update — on all five listeners while it is. Nothing is lost: submitting or cancelling the form already triggers its own redraw the moment it closes, which picks up whatever arrived in the meantime.

v1.8.413

Fixed real bug: building walls didn't actually land on the grid

Reported live with a screenshot, root-caused by inverting the isometric projection on a wall's actual bottom corner: it landed at fractional tile coordinates like (21.75, 5.75), not the half-integer positions the grid overlay/roof/floor are all drawn at. A wall's bottom edge is the roof's own footprint shifted straight down in screen space by a flat `wallHeightPx` — a real height cue, but a pure pixel offset with no tile coordinate behind it. Working out what that offset does to the underlying (tx,ty) math: a pure vertical screen shift with no horizontal component only happens when tx and ty increase by the same amount, which shifts the pixel position by a multiple of the tile height (24px) — so `wallHeightPx` has to be an exact multiple of 24 or the wall's base (and the floor plane hanging off it, and every per-tile wall segment) sits at a fractional, off-grid position no matter how precise the horizontal placement is. None of the old values (castle 40, bank/rune shop/workshop 30, windmill 46) were multiples of 24. Snapped to the nearest ones (24 or 48) via two shared constants instead of per-building magic numbers.

v1.8.412

Fixed real bug: hovering a building's revealed floor flickered exterior/interior

Reported live, reproduced and root-caused directly (not guessed): the floor was rendered as its own top-level div, a DOM SIBLING of the building wrapper, not a descendant. CSS `:hover` only propagates to an ancestor when the actually-hovered element is a real descendant of it — hovering a floor pixel never counted as hovering the building at all, so the hover glow only lit up when the cursor happened to land on a wall/roof pixel instead of a floor pixel, and those interleave across the same small area once an interior is revealed. Moving the cursor "anywhere over the floor" constantly crosses that boundary, flickering the glow on and off. Fixed architecturally: the floor is now a real child of the building wrapper, exactly like the walls already are (they already extend past the wrapper's own roof-sized box while staying descendants — same treatment, extended to the floor). Found and fixed a second, related bug in the same pass before it could ship: the wrapper's own hover effect used to set `filter: brightness(1.3)` directly on itself, which creates a new CSS stacking context for the duration of the hover — that would have trapped a bot standing inside a hovered building from correctly depth-sorting against its walls (the real per-tile z-buffer fix from v1.8.407), silently reintroducing that exact bug narrowed to "only while hovering." Moved the brightness onto the same per-child selector the glow already correctly used, matching how the persistent selection outline was already scoped.

v1.8.411

Grind: the river is a real obstacle now, with one bridge crossing

Phase 1 of the building-logistics/terrain roadmap. The river rendered but never actually blocked movement — bots walked straight through it. Now the river's full footprint blocks (unlike a building, water has no walkable "interior," so this is a new helper, `_riverBlockedTiles`, rather than reusing `_buildingWallTiles`'s perimeter-only logic) except one fixed crossing point real OSRS causeway/bridge-style. Added a visible plank-brown bridge plane at the exact same tile range the collision gap uses, so the crossing reads as an actual bridge rather than an unexplained gap in an otherwise-solid river. Currently nothing sits on the far side of the river (it's the map's eastern edge), so this is mostly inert today by design — it sets up a real, meaningful chokepoint for a planned dungeon entrance later in this same roadmap, rather than being pure decoration.

v1.8.410

Grind: edit-mode save/restore now covers every building, plus a real stale-collision bug

Phase 0 of a building-logistics/terrain roadmap (more buildings, a real dungeon, multi-level rooms coming in later versions) — a prep fix so those don't all hit the same gap. Map edit mode's save/restore used to name castle/bank/rune shop individually; the Workshop (added v1.8.364, genuinely draggable) was never added to that list, so dragging it "worked" for the session and silently reverted on reload. Generalized to walk the building registry itself, keyed by id, so every future building is covered automatically — nothing to remember to update by hand anymore. Also found and fixed a real related bug in the same audit: the one-time boot collision calculation ran before saved drag positions were ever applied, so a restored layout looked right but bots' actual pathing/collision stayed computed from the DEFAULT positions until the next live edit — moved buildings could be walked through, or block empty space where they used to be. Now recomputes collision after restoring positions, so a saved layout is correct from the first render. (If you'd previously dragged the castle, bank, or rune shop in edit mode, that one-time layout tweak needs re-doing once — this data was always documented as cosmetic and trivially redone, never backed up.)

v1.8.409

Fixed real bug: building hover flicker on the Grind map

Reported live. Root cause: `.iso-building:hover` lifted the building 4px via `transform: translateY` — applied directly to the same element whose `:hover` state controls it. Near the bottom edge of a building's hit box, hovering shifts the box up, which can carry that edge out from under the cursor, un-hovering it, which removes the shift, dropping the edge back under the cursor, re-hovering it — a self-reinforcing loop for as long as the cursor sits in that band, and any real mouse has enough natural jitter to keep re-crossing it. Every other hover effect on the map (terrain nodes, bots) scales from center instead, which is flicker-safe since growing moves every edge outward, toward the cursor, never away — buildings were the one exception. Dropped the lift; brightness + the existing drop-shadow glow (already geometry-neutral) carry the hover feedback on their own.

v1.8.408

Fixed two real "wiped again" sync bugs: bots and Sheets IDs

Reported live: bots got wiped a second time (the xp/credit backfill from v1.8.377 correctly rebuilt the numbers, but the actual bot objects have no such recovery), and separately the Wex tracker / Product Lookup sheet IDs kept disappearing from Settings → Integrations. Both traced to the same root cause as the original v1.8.343-345 incident: a stale cloud snapshot silently overwriting real local data. Bots: the existing "cloud has fewer bots than local → keep local" guard (v1.8.343) only ran once a `preferLocal` freshness check passed — but a wipe itself calls `saveFarm()`, which marks a fresh "local edit" timestamp, so `preferLocal` came back true and skipped the guard entirely, meaning cloud's still-good copy was never consulted to recover the wipe. Fixed by running the bot-count comparison unconditionally, so a cloud copy with MORE bots than local now wins even when `preferLocal` is true. Sheet IDs: same pattern as the existing decline-reasons/AI-notes protection missed — a stale cloud settings snapshot predating a saved sheet ID was blindly replacing settings wholesale; a fallback in `applySettings()` papered over it in memory each session but never actually persisted the fix or re-uploaded it, so it silently re-broke every time a stale cloud copy landed. Now preserves the locally-known ID for real and re-queues it for upload when cloud's copy is missing one, same treatment as the other two protected fields. Also confirmed live: the Camera DB sheet ID is intentionally hardcoded (not user-editable, not stored in settings), so it was never exposed to this bug.

v1.8.407

Grind: real per-tile z-buffer sorting for building walls

Direct ask, and the actual root cause of "bots walking over the castle walls" and the odd overlap in a screenshot: a building's whole wall used to be ONE shape with a single fixed depth value (its own front corner) — correct for a bot standing exactly there, wrong for every other point along it. The castle spans 12 tiles, so a bot anywhere else along that wall would render in front of or behind the ENTIRE thing regardless of whether it was actually in front of or behind that one specific spot. Rebuilt wall rendering as real per-tile segments, each with its own depth value computed exactly like a bot or node at that tile gets — so it now interleaves correctly with anything passing at any point along a wall, not just at one corner. The building's own wrapper no longer forces a fixed depth on everything inside it either, which is what made per-segment sorting possible — two small decorative pieces (the windmill's sail, the edit-mode drag outline) needed their own depth values updated since they'd been quietly relying on that.

v1.8.406

Grind: the Banker belongs inside the bank — fixed the fade condition instead of moving him

Correction to v1.8.405: moving the Banker outside the bank's door was solving the wrong half of "the bank reads as permanently see-through" — he belongs inside, always has. The real fix is in the fade condition itself: a building's walls now only fade for a real BOT visiting (or the manual "View interior" toggle), never for an NPC who just lives there permanently. An NPC standing inside a building it's tied to is normal and expected, not something that should keep the whole building looking occupied. Also removed the now-unused NPC-occupancy helper this change made dead code.

v1.8.405

Grind: five real fixes — geometry, a leftover decal, and left-click's menu

Real feedback on v1.8.404, five separate things. (1) Buildings looked like the walls sat below the floor — the floor was rendering at roof height instead of ground level, where the walls' own bottom edge (and every bot/piece of furniture) actually is; shifted it down to match. (2) The castle's roof had a door-shaped and two window-shaped decals baked into its background — leftovers from before the building went isometric, when it was one flat top-down diamond with no walls of its own to put a door on; removed, flat colour now like every other building's roof. (3) The bank read as permanently see-through — the Banker NPC stood just inside its door, keeping the building flagged "occupied" almost all the time; moved just outside instead. (4) Left-click on a building stopped opening its menu (right-click kept working, which ruled out the menu code itself) — most likely cause: the map's own pointer-capture-based pan handling was retargeting the click away from the building before it ever reached its listener. Rewired to track what's under the pointer at pointerdown, before capture engages, and dispatch from there instead of trusting the native click. (5) The real, foundational one: walls didn't line up with the actual grid squares because the building-footprint math was using each tile's CENTER instead of its outer edge, landing every wall half a tile short of the real boundary on every side — fixed for buildings, the river, and the new floor planes all at once, since they all share the same underlying projection function.

v1.8.404

Grind: the selection glow actually hugs the building now, and interiors are real rooms

Real feedback on v1.8.403, three parts. First, a genuine bug: the selection glow was a box-shadow sized to the building's bounding BOX, but a building's actual visible shape is a clip-path diamond roof plus angled walls — the glow rendered as a rectangle around empty corner space instead of hugging the roof/walls. Fixed with filter:drop-shadow instead, which traces the real clipped silhouette exactly. Second, an interior no longer reveals plain outdoor grass where the floor should be — every enterable building now has a real floor plane sized to its own footprint, plus a little furniture (a throne and table in the castle, a counter by the bank, a shelf in the rune shop, a stack of planks in the workshop) so it actually reads as a room, not an empty box. Third, and the bigger change: interior tiles now genuinely work like outdoor ones. Aubury moved from standing outside the rune shop's door to actually inside it (both shop buildings grew a bit to have real interior room), and the Construction skill's sawmill node moved from open ground to sit inside its own Workshop building — before the Workshop's built that's just open ground like normal, but once it's built, real walls go up around the exact spot a bot was already working at, and its work area switches to the building's genuine walkable interior instead of a plain radius that could hand out a wall tile as a destination.

v1.8.403

Grind: buildings get a real select-and-menu, plus an on-demand interior view

Direct ask, three parts. First, clicking a building (left or right) now selects it — a real, persistent glow around it, not just a hover flash — and opens the same context menu either way, instead of left-click jumping straight to a sidebar occupant list while right-click opened a near-identical one-item menu. Second, a new "View interior" toggle in that menu (and in the sidebar panel) makes a building's walls fade on demand — the exact same in-place wall-fade the map already uses when a bot happens to be standing inside, just triggerable any time rather than only by accident, and deliberately not a separate flat view — it's the same isometric building, just see-through. Third, hover feedback on buildings got a proper glow instead of a faint brightness tick. One real bug turned up building this: a plain click opening a context menu needs to stop the click from bubbling to the document's outside-click-closer, or the menu it just opened tears itself back down the same instant — the exact race a v1.8.316 comment already documented, just newly relevant now that left-click opens a menu too.

v1.8.402

Grind: a bigger map, a full relayout, a new building, and a real bug fix

Direct ask: grow the physical map, tidy up the layout, add more visual variety and buildings, and audit that buildings actually work as intended. The tile grid grew 36x20 -> 48x28, and every node, building, the river, and all three NPCs were repositioned into loose zones (a town cluster around the castle/bank/rune shop, a grove and river along the right edge, a quarry and farmlands along the bottom, with Runecrafting/Hunter/Agility scattered through the open ground the way real OSRS scatters them) — checked with a pairwise-distance script rather than eyeballed, so nothing's crowded despite everything moving at once. The building audit turned up a real bug: the Construction skill's Workshop building has been rendering completely invisible since the day it shipped (v1.8.364) — correct collision, zero visual footprint, because `.iso-workshop` was referenced in the code but never actually defined in the stylesheet. Fixed, and given a proper look matching every other building. A new Windmill joins the map near the farmlands — decorative only, with a slowly rotating sail, solid collision like the castle used to have before its own door. 14 small decorative props (trees, rocks, bushes, flowers) scattered around for texture between the actual gameplay nodes. Also added a "Reset map" button (a real gap until now) since this much repositioning could otherwise collide with anyone's previously-saved Edit Map drag positions.

v1.8.401

Grind: the interface panel is a compact corner box now, not a bar across the screen

Real feedback on v1.8.400: even translucent, a full-width strip still ate a whole horizontal band of the view — and explicitly not by collapsing it away (that was already an option, and re-opening it would just re-create the same problem). The actual OSRS interface isn't a bar spanning the screen — it's a small fixed box in one corner, with the game view open everywhere else. Rebuilt it as exactly that: a 300x400 box docked to the bottom-right, leaving the map fully open across the rest of the viewport instead of losing an entire strip. Getting content to actually fit a box that size meant reverting a few things back to how this exact sidebar worked before it was ever widened to full-bleed (v1.8.342): tabs go icon-only again (hover still shows the label), Grand Exchange rows reflow instead of cramming 5 columns into 300px, and Hiscores rows drop the activity column that has nowhere to go at this width. The collapse toggle still exists as a secondary option, now sized to the box's own corner instead of centered under a full-width bar.

v1.8.400

Grind: the "floating HUD" panel actually floats now

A live screenshot of v1.8.399 confirmed it: the panel was an overlay in the DOM but looked exactly like the old docked bar — full-bleed edges, solid background, a shadow too faint to notice, no visible sense that the map continued behind it. Worse, because it's opaque, making the panel taller than before (250px → 360px) just shrank the always-visible map (410px → 300px) — the opposite of the point. Fixed properly this time: the panel is now genuinely translucent with a background blur, so the map is visibly there behind it, softened, even fully expanded — that's what actually reads as "floating over," not a shadow at the seam. It's also inset with a real margin now instead of running edge-to-edge, so map shows around every side. The collapse toggle moved from a bottom-right corner (which sat right on top of the Hiscores/Trade list's own scrollbar — probably why it was invisible in the screenshot) to a top-center pull-tab clear of any panel content.

v1.8.399

Grind: the map is finally full-viewport, with the interface as a floating HUD

Real feedback, twice: a decluttering pass a while back (bigger interface bar, bigger cards) helped some but the tab still read as cramped — because every attempt was trying to win back space inside a layout where the map and the interface bar permanently split one fixed height. This stops splitting it at all. The interface panel is now an absolutely-positioned floating overlay docked to the bottom of the map — not a section sharing height with it — so the map genuinely fills the whole viewport for the first time, and the panel itself could grow taller (250px → 360px) for free, since floating over the map costs it nothing. A new collapse button (bottom-right corner) slides the panel fully out of view for an unobstructed map when you just want to watch bots work. Real layout surgery in an area with a track record of live-only bugs (several past Grind bugs were only ever caught in a real browser, never by a syntax check) — flagging clearly that this needs a live look before leaning on it.

v1.8.398

Dust Level grading table added — and a real fix to Unique Product Notes' own description

Two things from working through a real example. First: NOVA's Dust Level dropdown now has its own grading table (Reference modal → Shutter/sensor tab, and in Ask AI's context) — but only 2 of its rows ("heavy dust and debris" = 8, "moisture and dust" = 9−) are confirmed against real NOVA data; the rest are a flagged best-judgment guess from the Stylebot theme project, used as-is per request but visibly marked "unverified" in the UI and tagged for Ask AI so it hedges those specific rows with "Not documented — my best guess" instead of stating a guess as settled fact. Second, a real correction to something documented a few versions back: Unique Product Notes do NOT need to be grade-neutral — a fault like a gritty focus ring gets reassurance-style Unique Product Note wording ("however, remains fully functional") while still capping the grade at 9−/8. The actual distinction is that Inspection Notes explain a gap between what the customer quoted and the real grade; Unique Product Notes just name the specific characteristic, regardless of whether it affects the grade.

v1.8.397

Ask AI: now names which NOVA field a suggestion actually belongs in

Real gap: Ask AI already had accurate knowledge of NOVA's fields (Dust Level is a dropdown, not a notes field; Unique Product Notes vs. the two separate Inspection Notes fields), but nothing told it to actually USE that when answering a fault question — it would give a grade and the approved wording, but never say which field to put it in. Told it explicitly: when answering a described fault/situation, briefly name the relevant NOVA field if it's genuinely useful (and skip it entirely for a plain grading question with no field angle to it). So asking about a dust-related fault now gets pointed at the Dust Level dropdown specifically, not left to figure out on its own.

v1.8.396

Reference Phrases: five approved Inspection Notes were missing their "affects images" variant

Checked the app's approved Inspection Notes against the real reference sheet. Five faults (lens chip front/rear, lens scratch front/rear, sensor marks) each have two approved variants — "doesn't affect images" and "affects images from [aperture]" — but only the "doesn't affect" half had ever been transcribed. Added the missing "affects images" variant for all five, verbatim from the source; the existing entries gained a "(no effect)" label suffix to disambiguate now that there's a second variant (existing installs get this renamed automatically, same as previous label migrations — nothing about the phrase text itself changed). Also tightened the Guide tab's "Writing Inspection Notes" wording to match the source more closely, and made explicit that an Inspection Note's actual job is explaining WHY the item was downgraded from what the customer described, not just "extra detail."

v1.8.395

Ask AI: a step-back review — knowledge overview, self-audit, and a smarter model where it matters

Stepped back and reviewed the whole Ask AI feature rather than fixing one thing at a time. Guide's "AI knowledge notes" section is now a real overview — "What Ask AI knows" — showing your reference-phrase count with a one-click jump to manage them, your note count, and a live knowledge-base size readout against the hard limit Ask AI can actually handle, with a visible warning once it's getting close (previously that only ever failed abruptly with no warning on the way there). A new "Check for contradictions" button audits the app's own accumulated phrases and notes against each other AND against the hardcoded grading tables — distinct from "Ask AI to reconcile," which compares against an externally pasted manual; this one needs nothing external, just checks what's already here. Also: the reconcile and self-audit tools (rare, higher-stakes, specifically trying to catch subtle inconsistencies) now use a stronger model than everyday chat questions — a real quality lever worth the extra cost precisely because neither is a daily action.

v1.8.394

Fix: AI knowledge notes could pile up as silent duplicates

Real logic gap: adding a note (auto-detected from a correction, or manually) never checked whether something already said essentially the same thing — confirm the same fact twice, weeks apart, and you'd get two near-identical notes forever, every one sent on every future question. Worse, if a fact ever genuinely changed, the old note never got superseded — it just sat alongside the new one as a live contradiction. Notes are now checked against the existing list (exact match, then a tight fuzzy match) before saving, and skipped if a near-duplicate already exists — the UI now says "Already noted" instead of falsely claiming something new was saved. The model itself was also told not to re-emit a correction for something already in its notes, so this is caught on both sides, not just client-side after the fact.

v1.8.393

Ask AI: pasting a bigger chunk of info now offers the smarter tool instead of just answering it

Previously, pasting several facts at once into Ask AI just got treated as one long question — the tool actually built for "a batch of facts, not one question" (Guide's "Ask AI to reconcile," which gives a categorized new/changed-phrase/fault/inconsistency breakdown with add buttons) already existed, but only reachable from Guide, with no bridge from Chat. Now, sending a message that looks like a bulk paste (roughly 450+ characters, or a real multi-line paste) pauses before calling the AI and offers a choice: ask it as one question anyway, or hand it straight to the reconcile tool (pre-filled, just needs its own explicit click there — cost stays a deliberate action). Deliberately conservative — an ordinary, even fairly long, single question won't trigger this. Also switched the chat box itself from a single-line input to an auto-growing textarea with Shift+Enter for a new line, both for a better typing feel and so a real multi-line paste is reliably preserved rather than depending on how a given browser happens to handle newlines pasted into a single-line field.

v1.8.392

Ask AI: NOVA reference corrected and expanded against the real live page

Worked back through NOVA's real DOM (the Stylebot theme project's own notes, built from confirmed live inspection) to check Ask AI's NOVA reference for gaps and mistakes. Found a real error: "Missing" was the wrong label — the actual checkbox reads "Missing/Not Received". Found a real gap in button names: the save buttons are actually labelled "Save Changes" and "Save Changes / Next" (not "Update Inspection"), and Save Changes / Next is specifically the default click when working through a quote's items back-to-back, not just a twin of plain Save. Added: the accessories table (a per-accessory Received/Replaced/Missing checklist, distinct from the single Missing Accessories summary field), the per-product "Start Inspection" vs "View Inspection" links, the real quote-level action row (Add Product, Add Note, Email Quote, Print All Product Labels — the one actually used day-to-day — Print Quote, Print Result, Print Receipt), and the real semantic weight behind each status checkbox (Grey Import is a neutral factual flag, not a problem, unlike the other three).

v1.8.391

Ask AI now knows your camera database, one model at a time

Real remaining gap: the camera database (battery, charger, where to find shutter count, common faults, things to look out for — per exact model) was completely invisible to Ask AI, despite being genuinely rich inspection knowledge. Bulk-including it wasn't an option — with 500+ cameras, even a one-line summary each would run well past the entire rest of the knowledge base combined. Instead, every question now gets checked for a confidently-named camera model (word-level matching, not a loose fuzzy guess — deliberately conservative, since injecting the wrong camera's data would be worse than missing the mention entirely) and, when one's found, just that camera's row gets added to the context for that one call. Ask about a specific model's common faults, battery, or shutter-count location and the answer is now grounded in the real per-model data instead of having nothing to go on.

v1.8.390

Ask AI becomes its own "Chat" tab, gets a real chatbot feel, and knows a lot more

Ask AI is promoted out of the Phrases tab into its own dedicated sidebar tab — one persistent input for both the first message and every follow-up, real per-turn Copy (previously only the newest answer was copyable), a distinct look for your side vs the AI's side, an explicit "New question" reset, elapsed-time feedback while it's thinking, and a notice once a long conversation's earlier turns have aged out of what the AI can actually remember. Because the thread now lives in its own tab that's built once and never rebuilt, the conversation genuinely survives switching tabs or closing/reopening the sidebar — not just restored-looking, actually still there. Phrases keeps a small "Ask AI instead" link for when nothing quite fits, which hands the query over rather than answering inline. Separately, and just as real: Ask AI's knowledge was missing two whole categories of documented material it should have had from the start — the shutter-count grading table (never wired in, so "what grade for 300k actuations" had no documented answer) and the entire Guide tab's procedural content (new-item checklist, batteries, battery door, Elucid, writing notes) — previously excluded on a scope call that didn't hold up once the actual point of this feature is to draw on everything the app already knows, not just grading tables. Both are now included, re-derived from the same functions Guide itself renders with so they can't drift out of sync.

v1.8.389

Sidebar UX audit: real conversation loss fixed, notes made findable, several small polish fixes

Acted on a full live-browser UX audit of the sidebar. Biggest fix: switching to the Search tab and back to Phrases (or closing/reopening the sidebar) mid-conversation used to silently wipe the visible Ask AI thread and your typed query, even though the underlying conversation was technically still there for a follow-up — now it's actually restored on screen, not just usable behind the scenes. Also: a new "Manage notes" link next to "Add a note" jumps straight to Guide's AI knowledge notes section (previously a saved note had no visible home anywhere in the sidebar); the note composer no longer pre-fills a quote into the same editable box you type into (a real bug — clicking into it repositioned the cursor mid-quote and silently corrupted your correction), showing it as read-only context above an empty box instead, with a proper in-context "saved" confirmation; the Phrases list now shows each entry's actual wording, not just its short label (fixes both an unreadable "Mite" category and no way to tell near-identical entries apart without copying each one); "Ask AI" now stays pinned to the bottom of the results so it's not buried under a long list of loose matches; "Did you mean" no longer suggests wildly unrelated phrases (its match threshold was looser than everywhere else in the app); and keyboard focus on the sidebar's rail icons is now clearly visible.

v1.8.388

Ask AI: NOVA's three note fields are no longer one blurred-together field

Real gap: NOVA actually has three separate free-text fields on a product — Unique Product Notes (hand-typed, reassurance-style, "doesn't affect image quality"), Inspection Notes (a different hand-typed field, customer-facing and shown in the quotation email, explains why the grade differs from expected), and a third read-only auto-generated summary confusingly also called "Inspection Notes" on the page. Ask AI's NOVA reference previously described these as one generic "notes field", and the Dust Level dropdown wasn't clearly distinguished from a notes field either. Both are now spelled out properly, cross-referenced against how this app already stores the same distinction (Reference Phrases labeled "Inspection Note ·" vs "... Unique Product Notes", and Grade Wizard's own `phrase` field) — so a question about which NOVA field to write something into gets answered correctly instead of guessed at.

v1.8.387

Sidebar: Lookup + Cameras merge into one Search tab; fix: Ask AI follow-ups could get stuck

Part B of the sidebar cleanup: the Lookup and Camera DB sidebar tabs are now one "Search" tab — a single search box spanning your item library, the Wex catalogue, and the camera DB, results grouped by source (a new shared unifiedProductSearch replaces three separate search implementations, including the substitute-item modal's own). Both sync-status bars stay, stacked at the top. Separately, a real bug: after Ask AI suggested a grade and you asked a follow-up, the reply box could get permanently stuck disabled — it only re-enabled on a failed request, not a successful one. Also added: a real grading rule for digital compacts (sensor debris visible down to f5.6, one small mark/low dust → Grade 7, NOVA Dust Level → "Dust and Debris"), and the sensor-debris grading table itself is now included in Ask AI's knowledge base for the first time.

v1.8.386

Ask AI now knows what NOVA itself has, and the Phrases tab absorbs the floating search

Two changes: (1) Ask AI's knowledge base gained a structural reference for NOVA — the separate Wex inspection tool this app paces the day around — covering its actual fields (Grade, Sensor Quality, Dust Level, Shutter Count, Firmware Version, Repair Required/Missing/Declined/Grey Import, and more) and action buttons, so a workflow question isn't answered purely in terms of this app's own fields. Same privacy boundary as everything else sent to the AI — field/button names only, never a specific quote or product. (2) The floating bottom-left phrase quick-search pill is retired — its fuzzy search and Ask AI thread now live inside the sidebar's Phrases tab instead, alongside the existing phrase list/management, so managing and searching phrases is one place instead of two.

v1.8.385

Ask AI: confirming an inferred guess now turns it into a real fact

Previously, the auto-save-as-note behaviour only fired for genuine corrections or new rules you volunteered — a plain confirmation ("that's fine") of something the AI itself had already flagged as "Not documented — my best guess" was just agreement, not new information, so it was never remembered. Now, confirming a previously-flagged inferred suggestion gets saved too — phrased as a confirmed fact ("Confirmed — wear to the base of a screen is Grade 8"), not a guess. Since every future question is grounded in the full notes list, this is what makes "you said before X was fine" recall actually work — the confirmed answer becomes real documented material, not something only remembered for the current conversation.

v1.8.384

Fix: Ask AI started 400ing as the reference library grew

Real bug, hit live: both Ask AI's question path and the manual-reconcile tool send the same serialized "current data" (Reference Phrases + Grade Wizard faults + battery table) as context, each capped at 20,000 characters independently. That cap was set when the reference library really was a few thousand characters — but it keeps growing, partly because the reconcile tool is doing its job and adding new phrases, and it quietly crossed 20,000 and started rejecting completely ordinary calls. Raised both caps to 60,000, comfortably past any realistic near-future growth — input tokens are cheap regardless of size.

v1.8.383

"Ask AI to reconcile" gets real loading feedback

A call reconciling a full manual can genuinely take 10-30 seconds — the button previously just said "Reconciling…" with no visible motion, easy to mistake for hung. Now shows a real spinning icon (new .mat-icon-spin, generic enough for any future long-running action to reuse) plus a live elapsed-seconds counter, and clears out whatever's left from a previous run immediately so stale results don't sit there looking current during the wait.

v1.8.382

Fix: a large manual's reconcile response got cut off mid-JSON

Real bug, hit live on the second real use: a manual with enough real content to produce a long change list got its response cut off at the 3000-token limit before the JSON finished, failing to parse entirely even though most of the response was complete and valid. Raised the token budget to 8000, and added a salvage parser as a safety net for if it still happens on an even larger manual — it walks the response tracking brace depth and keeps every complete entry, discarding only the one that got cut off mid-way, instead of throwing away an otherwise-good response over a missing closing bracket.

v1.8.381

Fix: "Ask AI to reconcile" rejected a genuine full-manual paste

Real bug, hit live on the very first real use: a full Inspection Process Manual paste comfortably exceeded the original 30,000-character cap and failed — exactly the "paste the whole document" use case this tool is meant to support, not an edge case. Raised well past any realistic single-manual-section size.

Also fixed the error handling that made this hard to diagnose in the first place: Ask AI's error toasts only ever showed the real server error message when it happened to mention "Daily," silently replacing any other real validation error (like this one) with an unhelpful generic "try again shortly." Now shows the actual message whenever there is one, across all three Ask AI call sites.

v1.8.380

Guide tab: "Ask AI to reconcile" — a real AI-backed manual update check

The "Check for manual updates" tool's local keyword/fuzzy check stays (free, instant), but there's now a real AI-backed option alongside it. Paste a manual section, click Ask AI to reconcile, and it does the actual comparison — new/changed Reference Phrases get a real "Add to my phrases" button (they're runtime-editable data, so this genuinely applies them), while new/changed Grade Wizard faults are shown with an honest note that they need a real code update, since those are built into the app itself, not stored data. It also flags genuine inconsistencies within the pasted manual itself, not just manual-vs-app differences.

v1.8.379

Grind tab: a decluttering pass — more room to breathe

Real feedback: the tab reads as cluttered and hard to read. The bottom interface bar grew (170px→250px, with a bit more overall height to match) so panels aren't fighting for a cramped 210px strip; card padding and gaps grew inside the Grind sidebar specifically (not the shared base card styles — Status/Strengths elsewhere in the app use the exact same classes and shouldn't shift just because Grind felt tight); stat/skill grids now fit fewer, slightly bigger cards per row instead of packing in as many tiny ones as technically fit; and the help/edit-map button row got clearer separation from the map below it. No changes to panels, tabs, or map mechanics — purely spacing and density.

v1.8.378

Ask AI becomes a real conversation, and captures feedback automatically

Ask AI was a one-shot call — no memory between questions. A reply box now stays open under every answer, and the full back-and-forth gets sent each time so the AI has real context for a follow-up ("what if it's on the rear element instead"). Capped at 6 exchanges server-side (never trusting the client's own trim) so a long thread can't grow the per-call cost unbounded — still governed by the same shared daily call cap as every other AI feature.

Messages that read as a correction or new rule, rather than a question, now get saved as a knowledge note automatically — the AI judges this itself and flags it with a machine-readable line the client parses out, same pattern the existing "suggested grade" chip already uses. The manual "Wasn't quite right? Add a note" link stays too, for anything the auto-detection misses.

v1.8.377

Fix: the real root cause — xp/farm backfill could run before history finished syncing

Confirmed live: the skill-XP/credits history backfill is guarded by a permanent one-time flag, so it only ever gets one real attempt. It used to run unconditionally ~1.5s after boot (via a fire-and-forget idle callback) — well before the cloud sync layer's much longer async chain (auth, Firestore persistence, several sequential reads) had actually replaced a stale or empty local copy of history with the real one. It ran "successfully" against that incomplete data, credited only a couple of items' worth of XP instead of 51 real days of work, and then permanently marked itself done — never touching real history again on any later reload.

Fixed by moving the trigger to fire only once the cloud sync layer confirms history has actually been merged (with a bounded fallback for a genuinely offline/signed-out session, so it still eventually runs rather than waiting forever). This is the actual root cause behind the Grind farm's bots/credits appearing to reset — not data being deleted anywhere, but a real boot-sequencing race silently locking in an incomplete backfill.

v1.8.376

Fix: a real CSP bug blocking Firebase Auth's own session iframe

Real bug, caught live via a browser console error: the CSP's frame-src only allowed accounts.google.com, but Firebase Auth needs to frame its own project domain (inspectiontime-ff96c.firebaseapp.com) for its internal session-persistence iframe — especially relevant since this app runs on a custom domain (inspection.dannylaw.dev) rather than Firebase's own default one. With that iframe silently blocked, auth state could resolve inconsistently across reloads, which lines up with reports of the Grind farm's bots/credits appearing to reset — a boot-time race where the app proceeds before auth has genuinely settled, rather than data actually being deleted anywhere. Added the missing domain to frame-src in firebase.json. Investigation ongoing — this is a strong, real candidate but not yet confirmed as the sole root cause.

July 2026

v1.8.375

Grind: a complete sprite redesign for equipped gear

Equipment used to be almost invisible — a hidden yield bonus and one flat 5px colour dot, only on 6 of the 15 bot activities. Bots now hold a real CSS-drawn tool shape matching their current activity (a pickaxe, an axe, a hammer, a chisel, and so on), still tinted by tier, and their torso gets a coloured edge reflecting the single best tier they own across every activity — so overall gear investment reads at a glance, not just what they're holding right now.

Equipment itself was extended to 8 more activities — firemaking (tinderbox), smithing (hammer), fletching (knife), crafting (chisel), hunter (trap), farming (dibber), herblore (pestle), construction (saw) — leaving only Agility without a slot, since real OSRS Agility genuinely uses no handheld tool at all, unlike every other skill here. Turael and Aubury also picked up a fixed, purely cosmetic weapon/staff accent of their own — no equipment mechanic for NPCs, just matching flavour.

v1.8.374

Grind: two real gameplay bugs — dead-code buffs and easy-to-earn titles

Fix: since v1.8.364 gave Construction (the last skill without one) a real bot activity, every skill now has a bot equivalent — which silently made the old "no bot equivalent -> buff every bot on the farm" path unreachable. Every completion had been buffing one specific activity's bots only, never the whole farm. Restored as a smaller secondary global buff alongside the full specific-activity one, so a bot on some other, currently-unbuffed activity still gets some benefit from any real inspection work.

Fix: bot titles (Bronze through Rune tier) were hardcoded absolute total-level thresholds from when there were only ~6 bot activities. As the roadmap grew that to 15, the real ceiling for a maxed bot roughly tripled (594 → 1485) but the thresholds never moved — so "Rune tier," meant to represent being most of the way to a max cape, had quietly become reachable at only about a third of true total level. Rescaled as percentages of the real ceiling so a tier keeps meaning about the same thing.

v1.8.373

Grades get plain-English names, not just numbers

A new grade-name mapping (10=As New, 9+=Mint, 9=Excellent, 9−=Very Good, 8=Good, 7=Well Used, 6=Heavily Used, Decline=Decline) now shows alongside the bare number everywhere a grade appears: the Grade Wizard's fault checklist and its live max-grade result, the shutter-count lookup, and Ask AI's "Suggested grade" chip. Also included in Ask AI's own context, so it can answer "what's grade 8 called" directly instead of only ever stating the number.

v1.8.372

Grind: floor-plan modal removed, NPC wander smoothed, better Edit Map feedback

The old flat floor-plan modal is gone outright — real feedback that it shouldn't exist alongside the in-place wall fade at all. Walls fading on the map itself (v1.8.365) is now the only way to see who's inside a building; the sidebar's occupant list stays, the separate popup view doesn't.

Fix: NPCs jumped instantly between tiles while idle-wandering instead of gliding like bots do — they were missing the same CSS transition bots use to smooth out that exact kind of single-tile hop.

Edit Map mode gets two feedback additions: a "Moving: X" tag names whatever you've actually grabbed, so a building and something standing inside/near it (e.g. the Banker over the bank) are never ambiguous; and a highlighted tile now follows the cursor live during a node/NPC drag, showing exactly which tile it'll snap to before you drop it.

v1.8.371

Grind: two Edit Map bugs — view snapping back, and no way to see inside a building while placing something

Fix: dropping a dragged node, building, or NPC in Edit Map mode snapped the whole map view back to zoomed-out-and-centred every time — a forced rebuild after every edit was accidentally re-running the "first ever open" reset instead of just re-applying whatever zoom/pan was already there.

Fix: dragging something toward a building (e.g. the Banker onto the bank) gave no visual feedback about where inside it you'd actually be placing it. The building's walls now fade in real time as you drag over it, live during the drag itself — not just after you've already dropped it.

v1.8.370

Hover-to-expand now works for regular search results too

v1.8.369's hover-to-expand only fired for an AI answer — real feedback that regular phrase results should get the same treatment. The whole results dropdown now grows on hover regardless of what's in it, and hovering a specific truncated result row lets its full phrase text wrap and show instead of staying cut off by the ellipsis (the dropdown growing taller doesn't fix a row's own left-right truncation on its own — that needed its own rule).

v1.8.369

Ask AI: full answers on hover, and a real suggested-grade chip

The quick-search results dropdown is deliberately capped at 320px so it never swallows the screen while typing — fine for a phrase list, cramped for a longer AI answer that ended up scrolling inside its own small box. Hovering an AI answer now grows the dropdown to show the whole thing at once.

When a single specific grade clearly applies, it's now pulled out into its own "Suggested grade" chip above the answer instead of being buried in a sentence — the AI is told to only include it when one grade genuinely applies (never guessed just to fill a slot), same honesty rule as everything else this feature does.

v1.8.368

Slimmer bot/NPC sprites, and the cape actually shows now

Real feedback: the little characters on the Grind map read as too bulky, and a bot's cape barely showed once it turned to face left. The torso was 70% of the sprite's width and the only part with a 2px border where everything else uses 1px — both are what made it look chunky. Slimmed the torso and legs, matched the border weight everywhere, and widened the cape slightly — it now has real room to peek out on both sides instead of a sliver. Affects bots and NPCs identically, since they share the same sprite parts.

v1.8.367

NPCs do something now, not just point at a tab

Right-click any of the three Grind NPCs for a real direct action instead of only "go to their tab": Turael rerolls your Slayer task on the spot, Aubury sells you 10 of the cheapest rune, and the Banker deposits every bot's carried inventory into the bank in one action (previously only available per-bot). Left-click still jumps to the full tab for anyone who wants the detail.

v1.8.366

Lumbridge Castle is walkable, and NPCs move on their own now

The castle was the one landmark on the map you could never actually see inside of — a permanently solid block with no door, unlike the bank and rune shop next to it. It now has a real east-side door and genuine walk-in collision (matching yesterday's in-situ wall-fade — walking a bot inside will show through the wall the same way), even though nothing lives inside it yet; an honest "accessible, not populated" fix.

Turael, Aubury, and the Banker were pure signposts until now — fixed in place forever, click-to-jump-to-a-tab only. They can be dragged to a new spot in Edit Map mode exactly like terrain nodes, and idle-wander near their spot on their own the same way bots do, so the map reads a little more alive even when you're not actively managing bots.

v1.8.365

Grind: buildings show who's inside in place — no popup needed

Real feedback: the "View interior" modal technically worked but wasn't the experience wanted — seeing inside a building in place on the map itself, not a separate popup. Every enterable building's walls now fade automatically whenever a bot's actually inside (previously bank-only, roof-only) — since a bot inside was already rendered at its real position the whole time, fading the wall is all it took to see it. The old flat floor-plan modal stays available as a secondary "View floor plan" option (sidebar panel, right-click menu) for whenever the map's zoomed out too far to see clearly through a faded wall.

v1.8.364

Grind: Construction joins the roster — the roadmap capstone (Roadmap Phase E)

The deliberate departure from every other skill added this roadmap: bots turn logs into planks (a normal processing activity, same shape as Smithing), but spending them isn't a repeating tier — it's a one-time build queue. Planks + gp unlock a real new building on the map, permanently, matching the real skill's own "build it once, it's yours" identity rather than something you grind repeatedly. First build: a Workshop, purchasable from the Slayer tab once you have 8 planks and 400gp at Construction level 10.

Deliberately shipped with just one building this pass — the map is genuinely crowded after this session's run of new skills (Hunter/Farming/Herblore/Agility all added nodes), and only one clear footprint turned up when checked. Construction also closes out the non-combat skill roadmap entirely — Mining through Construction, 15 of OSRS's skills now have a real bot activity, with only Thieving explicitly left out (weak fit, no natural target in this world) and full combat still out of scope.

v1.8.363

Grind: Agility joins the roster — the first skill that changes movement (Roadmap Phase D)

Five real OSRS courses (Gnome Stronghold 1, Varrock rooftop 30, Wilderness Course 52, Seers' Village rooftop 60, Prifddinas 75), all paying the same fungible Marks of Grace — a deliberately low yield rate, since Agility is xp-training in real OSRS, not a resource farm.

The real payoff: a bot's own Agility level now genuinely changes how it moves — running drains real energy slower and regenerates faster the higher the level, scaling smoothly to half drain / 1.5x regen at 99. First mechanical link between a skilling activity and the cosmetic movement layer in this whole system, rather than a skill that's just flavor with its own map node. Found and removed a real dead-code duplicate while wiring this in: _botMoveSpeed was declared twice in grind-view.js — the first copy was 100% shadowed and has been silently dead for a while.

v1.8.362

Grind: Herblore joins the roster (Roadmap Phase C)

Standard processing shape, exactly like Cooking/Smithing/Fletching — a bot brews whichever potion its level allows from Farming's clean herbs at a new apothecary's table. Real herb-to-potion pairings for the six herbs Farming actually grows: Attack potion (Guam, level 3), Strength potion (Tarromin, 12), Prayer potion (Ranarr, 38), Super strength (Kwuarm, 55), Super restore (Snapdragon, 63), Zamorak brew (Torstol, 78). Potions trade through the Grand Exchange like every other bank item. No new mechanism needed — the roadmap called this the cleanest fit of the remaining skills, and it was.

v1.8.361

Grind: bots get AI-generated names

Buying a bot still assigns an instant, reliable name from the curated OSRS-name pool — that never changes and purchase never blocks on a network call. A moment later, if the AI responds, the bot renames itself to something freshly generated in the same spirit (real OSRS content-creator names — Zezima, Woox, B0aty). Any failure is silent; the pool name was never a placeholder, just the reliable default.

Deliberately only fires on a real bot purchase — never a tick or timer — so it can't turn into a runaway cost. Shares the same daily call cap as Grading's Ask AI. Cloud Function refactored (functions/index.js) to share the owner-check/cost-guard/Anthropic-call logic between both features instead of duplicating it.

v1.8.360

Ask AI: describes faults directly, flags guesses honestly, and now has a memory

Handles descriptions, not just questions. Type what you're actually looking at ("chip on the front element, doesn't show in shots") instead of phrasing it as a question — the AI now identifies the matching fault, states the grade first, then the approved phrase, in that order.

Inferred suggestions, clearly marked. If something genuinely isn't in the reference material, it can now reason by analogy to similar documented faults instead of just refusing — but that part of the answer is always prefixed "Not documented — my best guess:", never blended in as if it were policy.

A real way to teach it things — this AI has no training loop; every call rebuilds its knowledge from scratch. New "AI knowledge notes" section in the Guide tab (and a "Wasn't quite right? Add a note" link right under any answer) lets you add free-form notes that get included in every future question. Decline-reason labels (just the menu options, not any specific declined item) are now included too.

v1.8.359

New: Ask AI, real natural-language questions in the quick-search

The floating quick-search now has a genuine "ask it like an AI" option — type a real question ("what's the max grade for a scratched lens?") and click Ask AI to get a direct answer, not just a fuzzy-matched phrase list. Backed by a real LLM call (Claude Haiku) through a new server-side proxy (functions/askGradingQuestion) that holds the API key — it can never live in the browser-side code, or anyone could pull it out and run up charges.

Privacy boundary, by design: the only thing ever sent is your question plus this app's own manual content — Reference Phrases, Grade Wizard faults, the battery-replacement table. Never anything from a live quote or history — no customer names, no quote numbers, no per-day work data. Answers are grounded in that material only; the model is told to say so honestly rather than invent a grade or rule it wasn't given. Never fires automatically — always an explicit click, so the (small) API cost is always a deliberate action, with a server-side daily cap as a backstop against a runaway bug.

v1.8.358

Floating quick-search collapses to an icon by default

Real feedback: now that an item's own name/right-click menu gives context-aware access to the same phrase content, the permanent full-width search box at the bottom-left was doing less relative work for the screen space it always cost. Collapsed to a small icon by default — click to expand, same search as before, collapses back once you click away with nothing typed or hit Escape.

v1.8.357

Fix: battery items no longer switch you away from the page

Real feedback immediately after v1.8.355 shipped: tapping a battery/charger item's name jumped to the Guide tab, which left wherever you were actually working. Fixed — it's a modal now (showBatteryReplacementModal), same as every other Grading entry point, so it never navigates away. Brand-filtered when the item name pins one (reuses the same brand detection Slayer tasks already use) — a Canon battery only shows Canon rows, not all 26 — falling back to the full table when it can't tell.

v1.8.356

Guide: paste an updated manual, see what's changed

New "Check for manual updates" section at the bottom of the Guide tab — paste any updated version of the source manual (or just a section of it) and it flags lines that look new or reworded compared to everything already transcribed into the app (Reference Phrases, Grade Wizard faults, the battery-replacement table). No stored full-text baseline exists to run a true diff against, so this is a heuristic classifier, not a verdict: exact matches don't show up at all, close-but-different wording gets flagged as "possibly changed" with the closest existing match shown alongside it, and anything unrecognisable gets flagged as "new." A "Copy summary" button turns the result into a plain-text list for handing to a manager or back to Claude for review — nothing is auto-applied, wording precision here matters too much for that.

Calibrated against real measured Fuse.js scores rather than assumed ones — a fault reworded in different words scored ~0.7 in testing, while a genuinely unrelated line's best candidate still scored ~0.86+, so the changed/new cutoff sits at 0.8, not the naive "low score = close" guess. A coarse keyword pre-filter also skips lines with no recognisable fault/part/brand vocabulary at all, since a whole-manual paste is mostly prose that was never meant to be transcribed here — flagging every non-matching line would bury the few that actually matter.

v1.8.355

Grading is a contextual modal again, opened per-item

Reverted v1.8.348's tab promotion — real use showed Grading works better as a lookup you reach for with a specific item in hand, not a tab you sit in, and it was one more slot in a tab bar that's grown to 8. Same content (shutter/sensor lookup, Grade Wizard), now a modal reachable from the sidebar rail's reference icon, an item's right-click menu, or the per-item shutter/camera-DB buttons.

New: tapping an item's name (previously inert) opens Grading pre-filtered to faults actually relevant to that item — a lens shows optics/rings faults, a camera body shows sensor/viewfinder/buttons/etc., and a battery or charger skips the Grade Wizard entirely (it has no coverage there) and jumps straight to the Guide's battery-replacement lookup instead. Same jump is also in the item's right-click menu as "Suggested phrases…". No new button was added to the item row — it already had 10.

v1.8.354

Guide: approved battery/charger replacement models, by exact part

Added a real per-part lookup to the Guide's Batteries section — 26 rows across Nikon, Olympus, Fujifilm, Sony, Canon, and Panasonic, mapping each exact original battery or charger to its approved Mathorn/Hähnel replacement (or flagging it as genuine-only where no 3rd-party equivalent is approved). Previously the app only said "Mathorn or Hähnel are OK" in general — this says which specific model for which specific part.

v1.8.353

New: a "Guide" tab for workflow reference (new items, batteries, Elucid)

Danny put together a standalone cheat-sheet page and liked its shape — step-numbered checklists, colour-coded do/don't cards, accordions, a jump-to table of contents — so it's now a real tab in the app instead of a separate file. Covers seven topics that had no home anywhere in DAILYALLOC yet: the full new-item-to-database checklist, caps & protective parts, batteries & chargers, a customer's own third-party battery, the missing-battery-door process (with the four site-specific paths — Frensham Road / Stores standard / Stores urgent / video cameras), finding a product in Elucid (with the real keyboard shortcuts), and the "always/ask this" framework for writing customer-facing Inspection Notes.

Deliberately does NOT re-transcribe the phrase library or the max-grade table — those already live in Reference Phrases and the Grading tab's Grade Wizard, and duplicating them here would just be a second copy to keep in sync forever. Rebuilt in DAILYALLOC's own sharp/mono look rather than the source page's amber/teal skin; exact-string markers (SEE SPARES, #NOTINCLUDED#, REPLACED, etc.) are click-to-copy inline, same reasoning as everywhere else these show up. Has its own filter box and a scroll-synced TOC. Hidden from the manager viewer, same as Grading/Audit/Grind — it's process documentation for whoever's doing the inspecting, not a report.

v1.8.352

Unmatched searches now get logged, for raising with a manager

Traced a real "no results" report — searching "lens debris" in the floating quick-search bar found nothing — to a genuine content gap, not a search bug: that fault ("Very heavy internal dust & debris within the optics") has a grade in the manual's table but no approved wording next to it, and the quick-search bar only searches faults that HAVE a phrase. The Grade Wizard's own search (which searches every fault, phrase or not) finds it fine — so the gap is real, just invisible from that one surface.

Rather than guess at wording that isn't in the manual, both search boxes now quietly log any query that comes back with zero results (once you've actually paused typing, not on every keystroke) — Settings → Data → "Unmatched searches" shows the running list with a one-tap Copy, ready to paste into a message asking a manager to get proper wording added. Local only, never synced; nothing is sent anywhere on its own.

v1.8.351

Smarter search: part-aware filtering + "Did you mean"

Both search boxes (Grade Wizard, and the new floating quick-search) got a real upgrade: every fault now carries a tags field naming the actual physical part it's about (viewfinder, optics, rear LCD, sensor, grips, rings, dial, buttons, ports, corrosion, cosmetic, odour) — finer-grained than the category grouping, which bundles rear-screen and viewfinder faults together since they're graded in the same table section despite being different parts. A query naming a specific part now filters to that part FIRST, before fuzzy-ranking the rest of the words — "debris in the viewfinder" no longer surfaces lens/optics dust results just because both mention "internal dust." Falls back to searching everything if a named part doesn't match anything, rather than going empty.

Also added a genuine "Did you mean" — when a search comes back with nothing, a much looser fuzzy pass across everything offers a single best guess as a clickable suggestion, instead of a dead end.

v1.8.350

New: a floating phrase quick-search, on every tab

A small always-on search bar now floats bottom-left on every tab (mirrors the existing "resume active quote" chip's bottom-right placement, offset clear of the left rail) — type a phrase or fault and get live suggestions with a one-tap copy, without needing to be on the Grading tab or open the sidebar's Phrases pane first. Searches a combined list: your own Reference Phrases library plus every Grade Wizard fault that has approved wording attached. Hitting Enter copies the top match directly for a genuine type-and-go flow; clicking any suggestion copies that one specifically. Hidden from the manager viewer, same as the rest of this personal tooling.

v1.8.349

Grade Wizard is now searchable — type a fault, get the grade and phrasing

The fault checklist gained a real search box: type something like "debris in the optics" and it fuzzy-matches (via the same Fuse.js already used for item/camera search elsewhere) straight to the relevant fault, across every item type at once — no need to pick the right type first. Every match (and every fault in the normal browse view) now shows its approved wording right under the checkbox, with a one-tap copy button, so ticking or searching for a fault surfaces both the grade AND what to actually type into Nova in one place. Around 50 of the ~70 faults have real manual-sourced phrasing attached; the rest (mostly Dust and a handful of dial/ring variants) show a grade with no phrase, because the manual's own Max Grade table lists them without matching prose wording — shown as absent rather than invented.

v1.8.348

Grading promoted from a modal to a real tab — front and center

Real feedback: the shutter/sensor lookup and Grade Wizard are the most-used part of the app, and were tucked behind a small rail icon and a modal. Promoted to a proper top-level tab — "Grading," right after Today — so it's a single click away instead of hunting for an icon. Same tools, same tab strip (Shutter/sensor, Grade wizard) inside, just living in the main content area instead of a 560px popup. The per-item grade button and the sidebar's quick-access shortcut both now jump straight to this tab (carrying the camera context through) instead of opening the old modal. Hidden from the manager viewer, same as Audit and Grind — it's an inspector's working tool, not a report.

v1.8.347

New: Grade Wizard, plus ~80 approved phrases from the Inspection Process Manual

Built from the new Inspection Process Manual (v1.0, Maz Caudle): pick an item type (camera body, lens, tripod, binocular/scope), tick every applicable fault, and the Grading modal's new "Grade wizard" tab shows the resulting worst — i.e. maximum — grade live, colour-coded the same way the existing shutter lookup already is. Faults are transcribed straight from the manual's own Maximum Cosmetic Grade table (~70 rows, grouped by category); a couple of genuine near-duplicate rows in the source itself were kept verbatim rather than silently reconciled, since resolving a conflicting grading rule is a policy call, not a transcription fix. Tripod is selectable but currently has no ticked faults of its own — the manual describes tripod-specific wording but never actually assigns it a grade ceiling, and guessing one felt worse than leaving it open. The Reference modal (Settings → rail → Reference, or the per-item grade button) now has a proper tab strip for this alongside the original shutter/sensor tools.

Also seeded ~80 new Reference phrases straight from the manual's approved wording — the exact-string workflow markers it repeatedly stresses (SEE SPARES, #NOTINCLUDED#, REPLACED, 3rd Party (Unbranded), Mathorn, Hähnel, Missing Figure 8 Lead, Missing Battery Door), plus new Unique Notes (delamination, rubber grips, chips, buttons/dials, ports, mounts, corrosion, tripods, focus/zoom rings, serial numbers) and Inspection Notes (grey import, lens chip/scratch/moisture/mite, sensor marks) that weren't already covered by the existing phrase library. Existing installs pick these up automatically — the seed step already appends anything with a new label without touching phrases you've edited.

v1.8.346

Backup/restore now covers Grind skill xp and the bot farm

Real gap found while reviewing what's actually protected after the sync-wrapper incident: the manual Backup/Restore feature (Settings → Data) only ever exported state/history/settings/actuals/itemLibrary — it never included xp or farm, exactly the two blobs that incident showed can be silently, permanently lost with no other recovery path at all. Both are now in the backup file and selectable individually in the restore dialog (bot count and credits/gp/Slayer progress shown, same as every other section). If you've never had a reason to click Backup before, this is a good one — a lost farm has no fallback now the way decline reasons or an archived day do.

v1.8.345

New: "Discard today" — a safe way to drop a duplicate day without archiving it

Closes a real gap surfaced by the sync-wrapper incident: the archived day from that incident survived correctly, but reopened as a duplicate "today" still dated the same day it was archived. The existing stale-day rescue only auto-clears a duplicate rolling in from a PAST date at boot — it had no answer for a same-day duplicate, and re-archiving it would have overwritten the real, already-good archive (History docs are keyed by date with no merge, last write wins). Settings → Data → "Discard today (don't archive)" clears today back to empty without touching History at all — for exactly this kind of stray/duplicate day, not real finished work. Confirm-gated, and undoable via the usual undo toast.

v1.8.344

Data-loss incident: recovery + real sync protections

Follow-up to v1.8.341's cross-file sync-wrapper fix, after two more real reports surfaced the same root cause: custom decline reasons vanished from the dropdown, and separately a full farm reset ("no bots... everything had reset"). Both are the same mechanism — saveSettings/saveFarm's cloud-queueing wrapper silently never installed for a whole session, so a later reload's cloud-vs-local merge trusted a stale cloud snapshot that predated the real local progress.

Recovery: custom decline reasons have no separate key — the label itself is stored directly in item.declineReason — so a one-time boot backfill (backfillCustomDeclineReasonsFromHistory) scans every historical and current item for a declineReason that isn't a built-in key and restores it verbatim. Genuinely lossless, not a guess. Bot/farm progress has no equivalent secondary record anywhere else in the data model, so that loss is not recoverable — said so plainly rather than promise a fix that doesn't exist.

Protections against a repeat: (1) the settings merge now unions customDeclineReasons from local and cloud instead of blindly trusting cloud's copy — safe because the list is purely additive in normal use. (2) The farm merge now refuses to silently replace a local farm with more bots than the incoming cloud copy has — bots never just disappear outside an explicit dismiss, so a local farm with real bots and a thinner cloud copy is treated as a strong signal the cloud copy is the stale one, regardless of what the freshness timestamps say (those timestamps themselves were never updated during the whole broken window, so trusting them alone on the first post-fix reload was still a coin-flip).

v1.8.343

Read a Sony camera's real shutter count straight off a photo

New panel in the Grading · shutter lookup modal (Reference table): "Read count from photo (Sony)" — pick a straight-out-of-camera JPEG or .ARW and it fills the shutter-count field in automatically, no manual menu-diving on the camera itself. Entirely client-side — the file never leaves the browser, no upload, no network call — parsing the same Sony MakerNote EXIF fields the public shuttertool.com site reads, adapted into this app's own Reference modal rather than linked out to. Covers the same DSLR/NEX/SLT/ILCE model range shuttertool documents, from the earliest A-mount DSLRs through the A7RM5/A1/A6700/FX3. DSLT-era bodies (electronic front curtain) show both the raw exposure count and the higher true shutter-actuation count, since those bodies can trigger the physical shutter twice per exposure. Unsupported models, edited/re-saved files (which strip the needed EXIF), and non-JPEG/ARW files all get a clear inline message instead of a silent failure.

v1.8.342

Grind interface panel moved from a side dock to a bottom bar

Real feedback: the side-docked panel "just changes size on each tab and looks really bad." Reflowed into a bottom-docked OSRS-interface-style bar instead — the map now runs full width, with the tab strip and its panel spanning underneath. Same markup/classes throughout (Bots/Skills/Slayer/Trade/Hiscores/Profile/Building panels needed zero changes), this was a pure CSS reflow — flex-direction column instead of row, tabs run left-to-right instead of top-to-bottom. Tab buttons show their label alongside the icon now too, since a full-width strip has the room a 44px-wide vertical one never did. Grids that folded down to a cramped 2 columns in the old 300px-wide dock now auto-fit as many columns as actually fit the full width.

v1.8.341

Two real crashes, caught live: a stale cross-file reference and a Farming gap

Real bug, first live-browser errors this whole Grind project has produced: saveFarm is not defined, thrown while app.js was still executing its own top-level code. Root cause — saveFarm's sync-layer wrapper (the "also queue a cloud write" pattern) was left behind in app.js when saveFarm's real definition moved to grind-core.js during the v1.8.326 file split; app.js loads first, so it was reading an identifier that didn't exist in any script yet. Because this was an uncaught error mid-script, it silently killed everything else queued after it in app.js for the rest of the session. Moved the wrapper to grind-core.js, right after saveFarm's actual definition, where it belongs.

Real bug: clicking into the Bots panel crashed with Cannot read properties of undefined (reading '0'). Farming (v1.8.336) is the one activity that isn't in either RESOURCE_TIERS or PROCESSING_TIERS — it has its own FARMING_TIERS table — and the bot card's tier-label helper never got a Farming case added when Farming shipped, unlike a couple of other spots that already needed the exact same fix at the time. Any bot actually assigned to Farming crashed the whole panel. Fixed with the same fallback pattern.

v1.8.340

Map edit mode, context menus everywhere, and a real sidebar-resize fix

Real bug, found while building this batch: switching sidebar tabs could still resize the whole map row despite the v1.8.308 fixed-height fix. Root cause — a flex item's default `min-height` is `auto`, which refuses to shrink below its own content's natural height no matter what `height:100%` says; a tab with enough content (Trade's long tier list, etc.) was quietly forcing the row taller. `min-height:0` on the sidebar/viewport fixes it properly.

Map edit mode: a new "Edit map" toggle (top-right, above the viewport) turns on drag-to-reposition for every terrain node and building. Dragging a building takes its door and (for the bank) its interior deposit marker along with it; releasing snaps to the nearest tile and recomputes collision/work-areas/pathing on the spot. Positions persist to this device (`dayalloc_grindMapOverrides_v1`) and survive a reload. No "reset to default" yet — a stray drag has to be dragged back by hand.

Context menus: terrain nodes, buildings, and NPCs all get a right-click menu now (bots already had one) — nodes offer their exact tile coordinates, buildings mirror their occupants/interior actions, NPCs jump straight to their tab.

v1.8.339

Fixed: buildings weren't clickable, bank's door didn't line up, hover feedback everywhere

Real bug: the bank's collision door sat on the SOUTH wall, but the decorative dark "doorway" rectangle painted onto the building (since v1.8.334, shared with the castle) has always been on the EAST wall — two different faces entirely, so the wall bots actually used to get in and out looked completely solid. Moved the functional door to match the existing graphic instead, and gave the rune shop a matching door graphic of its own (it never had one).

Real bug: clicking a building often silently did nothing. The map's drag-vs-click threshold was 3px — well within ordinary mouse/finger imprecision for a plain click on a small target — so an intentional click on a building could get misread as a drag, and the very mechanism that swallows a drag's trailing click would then eat that same click. Raised to 6px.

Hover feedback added throughout the Grind map: buildings lift and brighten, terrain nodes scale up and pop to the front, bots and NPCs get a clearer scale-up alongside their existing outline, and bot cards in the sidebar highlight on hover — previously only a handful of these gave any indication they were interactive at all.

v1.8.338

Real terrain-object nodes, replacing plain icon markers

Every resource node on the map (rocks, trees, fishing spots, the anvil, altars, the firepit, the fletcher's table, the jeweler's bench, hunter traps, farming patches, even the bank's deposit marker) used to be the same plain icon-in-a-circle, just with a different glyph. Each now renders as a small hand-drawn CSS terrain object shaped like the real thing — a rock cluster, a tree with a trunk and canopy, ripples on a fishing spot, crossed logs under a flickering flame, and so on — built from OSRS wiki reference rather than hotlinked images (this exact system already hit broken hotlinked node icons once, back in v1.8.311, and deliberately skipped hotlinking equipment for the same reason). Runecrafting's altars go one step further: each one's glowing orb is tinted to match its real rune colour (pale for Air, deep red for Blood), the one place a node's look varies by tier and not just its tooltip.

v1.8.337

Real building collision, bot pathfinding, clickable buildings + interiors

Real feedback: the bank and rune shop LOOKED like real buildings but had no actual collision at all — a bot could walk straight through a drawn wall from any side. Both now have a real wall perimeter with exactly one door tile each; only the door lets a bot actually pass. That exposed the map's movement logic wasn't real pathfinding either — it just tried to step directly toward the destination and gave up if blocked, which would have left bots stuck outside a narrow door forever. Replaced with a real BFS pathfinder: the moment a bot's destination changes, it computes an actual route around every obstacle (once), then just walks that route tile by tile — matching the wiki's own description of how OSRS pathing works on open ground.

Buildings are clickable now too — the castle, bank, and rune shop each open a sidebar panel listing exactly who's inside (checked against their real tile position, not a rough guess). The bank and rune shop additionally get a "View interior" button: a real top-down floor plan showing the walls, the door, and every bot currently standing inside — a genuinely different view rather than a zoomed copy of the outdoor map. The castle stays a solid, unenterable landmark, so it only shows the (always-empty) occupant panel, no interior button.

v1.8.336

Farming joins the roster (roadmap Phase B)

The best-fitting remaining skill on the researched roadmap, and the first genuine architectural departure from every other gather activity so far: real OSRS Farming grows on elapsed real time, not a per-minute rate. A bot plants a seed into one of a few patches; each patch matures at its own real-world timestamp and is auto-harvested the next time the app reconciles — exactly the same "catches up correctly even after being closed for hours" behaviour every other part of this idle sim already has, just applied to a fixed maturity time instead of a continuous rate. Real level/timing spread researched off the wiki: Guam (level 9, ~20 min) through Torstol (level 85, ~100 min), six herb tiers.

Seeds are bought via the Grand Exchange like any other resource; clean herbs sell back through it too — Farming gets its own Trade tab group, flattening its two-key-per-tier shape (a seed, a harvested herb) into the same one-row-per-key list every other group already uses.

v1.8.335

Hunter joins the roster (roadmap Phase A)

First skill off the researched roadmap for the remaining non-combat OSRS skills. Hunter is structurally identical to Mining/Woodcutting/Fishing — a bot traps whichever creature its real OSRS level allows (bird snares 1, rabbit snaring 27, falconry 43, maniacal monkeys 60, herbiboar tracking 80), no new mechanism needed at all. Five trapping spots scattered across the map, same authenticity reasoning as Runecrafting's altars — real trapping genuinely happens at different locations too. Furs/feathers/meat sell through the Grand Exchange like everything else.

v1.8.334

Real feedback: NPCs redone as players, real isometric buildings, capes

NPCs no longer live in a hover-to-expand HUD box — the Slayer Master, Aubury, and a new Banker all stand on the map as real characters, built from the exact same doll (head/torso/legs) as a bot, each with their own cape colour. Click one to jump straight to what they're for (Slayer task, rune shop, bank).

The castle and river hadn't actually been updated to look isometric — the castle was a flat single-colour diamond, the river a generic diagonal gradient with no relationship to the tile grid at all. Both fixed: a new generic building renderer draws a real roof plus two visible wall faces (the standard "2D drawn to look 3D" isometric technique), and the river is now a genuine tile-aligned region along the same edge Fishing's own spots already sit at.

Two new buildings: a bank building (built to enclose the existing bank tile, so every bot's already-existing banking trip now naturally reads as walking into it) and a rune shop near the altars. The bank building fades its roof while a bot's tile is actually inside its footprint — a real "z-buffer for entering rooms," toggled every movement tick — so you can see the bot standing inside instead of it disappearing under an opaque roof. The castle stays a solid, unenterable obstacle (unchanged) and doesn't need this.

Bots and NPCs both get capes now. A bot's only shows once it's actually reached a title tier (colour-matched — gold for the Max cape), giving the existing badge system a real visual payoff instead of just a text label; NPCs wear a fixed colour purely for character styling.

v1.8.333

Crafting joins the roster, real gem finds while Mining

'Crafting' has sat unused since v1.8.310, same gap Fletching just closed. Researched via the OSRS wiki: while Mining any rock, there's now a small chance of a bonus uncut gem (sapphire through onyx, weighted toward the common ones) — a real OSRS mechanic, and genuinely not gated by Crafting level in the real game either, so a low-level miner can still find a diamond. Crafting bots turn a gem plus a gold bar into a ring at a new jeweler's workshop, using the real level ladder (Gold ring 5, Sapphire 20, Emerald 27, Ruby 34, Diamond 43, Dragonstone 55, Onyx 67) — and correctly falls back to whatever gem's actually in stock, all the way down to a plain gold ring, since the existing tier-selection logic already checks real bank stock, not just level.

New Mining tier (gold ore, level 40, its real position between Coal and Mithril) and Smithing tier (gold bar, level 40) feed it. Also fixed a real, separate inaccuracy found while verifying these: Mithril bar's Smithing level had been 55 since v1.8.317, but the real OSRS level is 50.

Also fixed two pre-existing map node collisions from Phase 3 (v1.8.329) — two Runecrafting altars actually overlapped the Mining rock row, caught only now because placing Crafting's own node prompted a proper pairwise check across every node instead of just bounds-checking each one individually.

v1.8.332

Fletching joins the roster, using real OSRS bow levels

'Fletching' has sat in the skill list since v1.8.310 as a global-buff-only skill with no bot mechanic behind it — closed that gap. Bots now turn logs (any Woodcutting tier) into bows at a fletcher's table, a second gather→process chain alongside Mining→Smithing. Researched via the OSRS wiki: unlike Smithing's simplified "same level as the raw ore" shortcut, Fletching uses the actual real bow level requirements (Wood shortbow 5, Oak longbow 25, Willow shortbow 35, Maple longbow 50, Yew shortbow 65, Magic longbow 85) — more authentic, not just a working number, since a bot's Fletching level doesn't have to match what its own gathered logs would suggest.

Deliberately no equipment slot — a real OSRS fletching knife isn't tiered the way a pickaxe or axe is, so skipping gear here is the more accurate choice, not an oversight.

v1.8.331

Diamond-tile grid overlay for the isometric map

The flat rectangular grid overlay was deliberately dropped when the map went isometric (v1.8.327) rather than shipping a straight port that wouldn't have looked right. Now back as a real diamond grid — drawn as SVG lines computed from the exact same tile-to-screen math the nodes and bots themselves use, rather than a CSS gradient's angle/spacing guessed to match, so it's guaranteed to line up with the actual tiles.

v1.8.330

Phase 4: Aubury the rune shopkeeper — a real second NPC

The Slayer Master's dialogue box (portrait + hover-to-reveal speech) was hardcoded to just him. Generalized into a reusable builder so a second NPC could share the exact same look/behavior instead of duplicating markup. Aubury — the real OSRS NPC who runs the Runecrafting shop — now stands near the new rune altar cluster, with his own flavor line reacting to current rune stock. Unlike the Slayer Master (a fixed corner HUD element), Aubury is a real map entity at his own tile — he pans and zooms with the map like any bot or node. Click him to jump straight to the Trade tab's Runecrafting group.

His portrait is hotlinked from the OSRS wiki on a guessed filename, same category as a couple of image guesses earlier in this project that turned out wrong — flagging honestly in case it renders broken and needs a fallback to a plain icon.

This closes out the Grind map project that started as "add zoom/drag": isometric rendering, Magic/Runecrafting, one node per resource tier, and now a second NPC.

v1.8.329

Phase 3: one map node per resource tier, and a bigger map

Mining, Woodcutting, Fishing, and the new Runecrafting activity now each get one map node per resource tier instead of a single shared spot — 6 rocks, 6 trees, 5 fishing spots, and 6 scattered rune altars (matching how real OSRS altars genuinely sit at different locations, unlike a mining/woodcutting site). A bot now paths to the SPECIFIC node matching whichever tier its level actually lets it work, not just any node for that activity.

The map grid grew from 24x12 to 36x20 to fit all the new nodes without crowding — the zoom/pan from a few versions back is what makes a map that much bigger still comfortable to look at. Node icons shrank slightly (34px to 26px) since some tiers now sit as close as 3 tiles apart on screen.

v1.8.328

Phase 2: Magic and Runecrafting join the roster

Two new bot activities, both non-combat (real combat stays out of scope, same boundary this system has kept since it was first built). Runecrafting is a gather activity like Mining/Woodcutting/Fishing — a bot crafts whichever rune its level allows, real OSRS levels (Air 1 through Blood 77). Magic is Low/High Level Alchemy — the two real non-combat "convert to gp" spells, consuming a rune (fire for Low, nature for High) straight out of the bank and paying gp, same shape as Firemaking's log-burning. Runecrafting producing the exact runes Magic consumes closes a real economic loop, same idea as the existing ore→bar→gear chain.

Both get the same Bronze→Rune equipment ladder as Mining/Woodcutting/Fishing/Cooking (a staff for Magic, a talisman for Runecrafting) — including the rare-find pity timer, which needed generalizing since it was only ever wired up for Cooking specifically. Both also count as real player skills now (Magic/Runecrafting), buffing their matching bot activity the same way landing Mining xp already buffs Mining bots.

v1.8.327

The map goes isometric (Phase 1 of a larger rework)

First phase of a bigger Grind-map project: the flat top-down map is now a real 2:1 isometric projection, matching how OSRS itself actually reads. Bots, the castle, the bank, and all six activity nodes are reprojected onto it — depth-sorted so anyone/anything further "back" on the map correctly renders behind whatever's further "forward." The tile grid, movement, and collision logic underneath are completely unchanged; only how a tile paints to the screen changed.

The isometric canvas is a fixed size that's usually bigger than the visible window on purpose — the zoom/pan added in v1.8.325 is what makes that work, and now opens already fitted to show the whole map. Also fixed a real bug this surfaced: node icons were being centered with a CSS trick (margin-top as a percentage) that silently resolves against the container's width, not its height — on a much-wider-than-tall map that pulled every node up by the wrong amount, which is why they looked mis-centered rather than sitting cleanly on one tile. Nodes are now circular markers, correctly centered via a transform that isn't fooled by aspect ratio.

Deliberately dropped for this pass: the flat grid overlay (doesn't make sense in isometric, needs a real diamond-tile version) and the castle's crenellated silhouette (now a plain diamond footprint). Both are flagged as follow-up art work. Next up: a new Magic/Runecrafting skill pair, many more map nodes (one per resource tier), and a second NPC running a rune shop.

v1.8.326

Grind tab code split into its own files

The Grind system (skill XP, the bot farm, Slayer Master, equipment, quests, the map/view) has lived inside app.js since it was first built, growing to roughly a third of the file. It's now split into two new files — grind-core.js (data/mechanics) and grind-view.js (the map and sidebar UI) — purely so updating the Grind system doesn't mean loading and editing the whole ~28k-line app.js. No behavior changes; everything still runs as plain global scripts loaded in a fixed order (app.js, then grind-core.js, then grind-view.js).

v1.8.325

Map zoom/pan, and three real bugs fixed from live use

You can now zoom in on the map itself — mouse wheel (toward the cursor), click-and-drag to pan, pinch on touch, plus explicit +/-/reset buttons in the corner. Panning is locked out until you've actually zoomed in, so the default view is unchanged.

Fixed a real bug: clicking a bot (or any other sidebar action) rebuilt the entire map's DOM every time, which destroyed every bot mid-animation and snapped them to their current tile instead of smoothly finishing the walk — looked like the game "jumped forward in time." The map is now built once and left alone; only the sidebar panel rebuilds.

Also fixed: switching a bot's activity (auto-borrow, manual reassign, rebalance) used to reinitialize it at a random tile in the new activity's area — an instant teleport. It now walks there properly from wherever it currently is. And resource-node icons, which sat at arbitrary positions that straddled several grid squares, are now snapped to the centre of a single tile and sized to match it exactly.

v1.8.324

Bots get real player models instead of flat colour squares

The map sprites were a single flat coloured square with a letter on it. Replaced with a small layered doll — head, torso, legs — that faces the direction it's actually walking (real horizontal movement only, same as OSRS characters not flipping on a purely north/south step). Torso colour still encodes the current activity, same as before.

Two new details tie the sprite to systems already built: a small colour chip appears next to a bot once it has any equipment for its current activity (bronze through rune, matching the real metal tiers), and Ironman bots now carry a small shield badge next to their sprite — matching how the real game marks Ironman accounts with an icon next to the name rather than a different model.

v1.8.323

More real OSRS mechanics: pity timers, run energy, wander, buff decay

Worked through the wiki's broader Mechanics category (skipping the ~90% that's combat/client-specific and doesn't apply here) and pulled several more real systems in. Equipment finds now have bad luck mitigation — the same pity-timer principle OSRS uses for rare drops, so a bot can't go forever on pure bad luck — and Cooking bots can now actually find their gear too (they could already buy/forge it, but the find roll was never wired up for them). Bots idle-wander a tile or two around their work spot while gathering/banking instead of standing perfectly still, the same bounded-wander real stationary NPCs use.

Running now costs real energy that depletes faster the more equipment a bot is carrying and regenerates while walking or standing still — a bot that's run its energy down has to walk until it recovers, same interplay the wiki's Weight/Energy pages describe. Buffs now taper off gradually over their last stretch instead of instantly snapping off at expiry. The bank display groups resources by activity (bank-tags-inspired), and the Slayer Master won't immediately hand back the same brand you just finished.

v1.8.322

Grid overlay, and bots route around the castle

Read the OSRS wiki's own Pathfinding page to check the tile-movement work against the real mechanics. The "move diagonally, then straight" stepping was already right; the one thing missing was collision — the castle is the only obstacle on the map, and until now bots walked straight through it. It's now a real blocked area: a bot whose path would cross it tries moving along just one axis instead, same spirit as the wiki's wall/pillar blocking tiles. Also added a faint grid overlay on the map matching the actual tile grid bots move on, so the movement is visible, not just implied.

v1.8.321

Real OSRS tile movement, and no more sidebar scrollbars

The map is now a real tile grid — bots walk (1 tile per 0.6-second tick) or run (2 tiles/tick, tied to how fast a gatherer already is) toward their destination, the exact same movement speeds and tick length as the actual game. Each node and the bank now has a small multi-tile work area instead of one exact point, and a bot's destination tile is picked by whichever tile in that area currently has the fewest bots already headed there — so a busy node spreads its crowd across several tiles instead of everyone piling onto one spot.

Also: the bot profile panel and the Grand Exchange had both grown into long single-scroll lists in the sidebar. The profile is now split into four tabs (Skills, Personality, Gear, Story) and the Exchange into one tab per activity — every panel now fits the sidebar's fixed height without needing to scroll.

v1.8.320

Fix: bots sharing a spot were teleporting instead of desyncing

Root cause of "still doesn't look random," diagnosed correctly from the description: only one bot per map spot was ever actually rendered (the rest hidden, since v1.8.314's "stack them, don't spread them out"), and adding per-tick jitter on top of that meant whichever bot was currently "the visible one" could swap to a completely different bot mid-cycle — instant, since hiding/showing isn't an animatable change — which read as one sprite teleporting, and a bot leaving a shared spot just vanished with no visible motion at all. Replaced the hide-one/show-one approach with an actual small grid: every bot sharing a node or the bank now renders simultaneously, each at its own stable slot, so a crowd actually looks like a crowd instead of a single flickering stand-in. Right-clicking a bot now always opens its own menu directly — no more "pick which bot" step needed.

v1.8.319

Bots get constant idle motion, not just an occasional walk

Follow-up to v1.8.318: bots' phase and speed were genuinely randomized, but each one only actually MOVED once per lap around the map — roughly every 15-20 ticks — and sat perfectly still the rest of the time, which still read as static rather than alive. Every tick now nudges each bot by a small random jitter around wherever it currently is (its gathering spot or the bank), so there's constant small motion instead of long stillness between rare full walks. Also widened how much bots' gather speed can vary (0.6x-1.6x, up from 0.75x-1.35x) so the pace difference between bots is easier to actually notice.

v1.8.318

Fix: bots still moved in sync despite the earlier desync pass

Real bug, reported live: bots still looked like they moved in lockstep even after the earlier desync work (v1.8.314) varied their gather speed. Root cause — every bot's cosmetic animation state hardcoded its starting PHASE to "walking to node," regardless of speed differences; since this state resets on every page load (it's session-local, not persisted), every bot marched through walk → gather → bank → deposit in the exact same order at the exact same moments each time the app opened, only drifting apart gradually as speed differences slowly accumulated. Now each bot rolls a random point anywhere in that cycle to start from (weighted toward "gathering," since that's where most of a real cycle is spent) — bots look properly independent from the moment the tab opens instead of needing time to visibly separate.

v1.8.317

A fully-fleshed-out bot simulation: personality, goals, equipment, quests

The biggest single expansion to the Grind tab since it was first built. Every bot now rolls one of six personalities drawn from real OSRS account-build culture — Efficiency Scaper, Skiller, Ironman, Merchant, Completionist, Grinder — each biasing four traits (ambition, sociability, patience, risk tolerance) that drive genuinely autonomous behaviour: bots now pick their own goals (reach a level, bank some gp, own a piece of gear, try every activity), spontaneously switch activities chasing them, and sociably trade surplus resources with each other — all pure rule-based decision-making, no AI calls involved. Ironman-archetype bots hard-refuse any trade, in either direction, on principle — real Ironman identity, not just a low dice roll.

Two new skills, Firemaking and Smithing, complete a real production loop: ore (Mining) becomes bars (Smithing) becomes equipment (the Forge) — the same six tier names (Bronze through Rune) used end-to-end for ore, bars, and gear. Bots can buy equipment with gp, forge it cheaper with bars, or get lucky and find a piece for free while gathering. Five real OSRS quest names are wired to concrete milestones for flavour, bots display a title (a level-tier band, a quest name, or a literal Max cape at 594 total level), and every bot's profile now has a life-story feed of everything that's happened to it. Hiscores gained a Quest points tab.

v1.8.316

Real Chrome-browser findings: GP-stuck-at-zero, map bugs, ctx-menu race

A live browser pass (Chrome Claude, following a written review prompt) caught several real bugs invisible to code review alone. The big one: GP could get stuck showing 0 forever despite real buys/sells succeeding — anyone whose farm data predated the gp currency (v1.8.306) had it sitting as `undefined` forever, so every calculation silently produced NaN (displayed as a deceptive 0, and NaN comparisons never blocked a purchase either). Now self-heals automatically wherever farm state gets touched, not just at boot.

Map fixes: the river was never actually visible (an opaque layer was drawn on top of it by CSS's background-stacking order); the castle was a flat block with no doors or windows; and the bank's position sat inside the castle's own footprint, so a bot mid-banking-lap visually stood on the solid castle floor instead of an outdoor spot. The multi-bot right-click picker's "pick a bot" rows did nothing — a same-click race with the menu's own outside-click-closer was tearing the newly-opened menu back down instantly. The NPC dialogue box's collapse-on-mouse-leave briefly showed a truncated, cut-off bubble (a height:auto transition can't actually animate — swapped for a fixed expanded height). Also: a renamed bot's card lost its name/level spacing, the stack badge read as a plain resource counter rather than "bots here," and Grind toasts could visually block clicks to the Trade sidebar underneath them.

v1.8.315

Grind tab audit: bug fixes, toast spam fix, and QoL additions

A full audit of the Grind tab turned up two real bugs — a right-click menu icon that wasn't in the Material Symbols allowlist (likely rendering broken), and the "Rebalance" button silently overwriting bots you'd manually assigned via the dropdown (it now only touches bots still on auto). Also fixed the "toasts are spamming my screen" complaint: repeats of the same message now collapse into one line with a (×N) counter, and the stack is capped at 5 — no more unbounded pile-up.

New: a "Bank now" button (on a bot's profile and its right-click menu) to deposit its inventory on demand instead of waiting for it to fill up; quantity inputs replacing the fixed "Sell 1" and lots-of-10 trading on both the bot profile and the Grand Exchange; and the buff flame icon now shows real minutes remaining on hover instead of just an on/off state. The in-app tutorial was also brought up to date to actually cover the right-click menu, rename, dismiss, and rebalance features from the last two releases.

v1.8.314

Bots desync from each other; stacked bots get an OSRS-style tile menu

Bots on the same activity no longer visibly run on identical cycles — each now starts its walk/gather/bank loop at a randomized point with its own speed variance, so a group of miners actually looks like independent workers instead of one synchronized blob. Replaced last release's offset-spread attempt at keeping overlapping bots apart: they now simply stack at the exact node/bank position (the way OSRS itself handles it), with only the "front" bot rendered and a small count badge showing how many are underneath. Right-clicking a stack lists every bot there by name first — pick one to get its usual management menu — instead of only ever being able to reach whichever bot happened to render on top.

v1.8.313

Bot management: stacking fix, right-click menu, self-chosen activities

Bots sharing a map node no longer render stacked exactly on top of each other — each bot gets a small deterministic offset around the node so a crowd of miners actually reads as a crowd. Right-click a bot (on the map or its sidebar card) for a management menu: open profile, rename, auto-choose activity, or dismiss (its carried inventory banks first, nothing is lost). New bots now pick their own starting activity — whichever's least staffed so far — rather than always defaulting to Mining, and a "Rebalance" button (once you have more than one bot) re-spreads everyone across activities in one click. Bot name generation is also now guaranteed unique even once the name pool runs out and it falls back to numbered suffixes.

v1.8.312

Fix: credit backfill could be silently lost to a sync race

Real bug caught live: a huge one-time credit catch-up toast (+28,611 from past work) fired correctly, but the displayed total stayed at 15 — the backfill ran in the boot-time deferred callback, which can fire before the cloud sync layer is ready (Firestore's initial read is a network round-trip with no fixed ceiling below the 1500ms idle-callback timeout). Without a matching "local edit" marker, the sync layer's cloud-vs-local merge moments later saw no reason to prefer the fresh local data and silently overwrote it with the stale cloud value. Both one-time backfills that pay out real currency (credits and the original history-to-xp backfill) now mark themselves as a local edit unconditionally — safe to do since each only ever runs once, so it can't reintroduce the old "restoring unsynced changes" toast spam this gating was originally added to prevent.

v1.8.311

Fixes from a real screenshot; autonomous bot trading

A real screenshot turned up two bugs: the Slayer Master dialogue box was rendering as a tall dark column instead of a small collapsed portrait (the hidden dialogue text, even at zero width, still computed a huge wrapped height that the box grew to fit before it could be clipped — now fixed height), and several hotlinked map/GP icons were broken (reverted to the Material Symbols glyphs that were already proven reliable, rather than keep guessing wiki filenames). Also new: bots now genuinely trade with each other — before a Cooking bot gives up and borrows itself out to Fishing, it checks whether another bot is carrying spare raw fish and buys it directly, gp changing hands at the going rate, only falling back to fishing itself if nobody has any to sell.

v1.8.310

Real OSRS skills, tiered per-bot inventories, bot buffs, trading

A big one. Your Skills are real OSRS skill names now (Mining/Woodcutting/Fishing/Cooking/Crafting/Smithing/Fletching/Construction), deliberately decoupled from item categories — every fully-inspected item advances the next skill in a fixed rotation instead of guessing a category match, and all skills start fresh at level 1. Landing xp in a skill that shares a name with a bot activity gives a short yield buff to bots on that activity; the other four buff every bot generically.

Resources are now genuinely tiered and level-gated, OSRS-style (copper → iron → coal → mithril → adamantite → runite for Mining, similar ladders for logs and fish) — a bot gathers the best tier its level allows. Bots have real per-bot inventories now (not a shared pool) with an actual carry cap; once full they bank everything into a shared bank. Cooking draws raw fish straight from the bank (no carry step, since the range sits next to the bank) and auto-switches to Fishing when there's nothing to cook.

Click a bot to open its full profile in the sidebar — all four skills, its inventory, lifetime output — with the ability to sell an item straight from inventory for gp or give it directly to another bot. The Grand Exchange now covers every tiered resource, grouped by activity. Also: game notifications moved to their own top-right, manually-dismissed stream (separate from the app's regular toasts), and the Slayer Master dialogue box collapses to just its portrait until you hover, giving the map its space back.

v1.8.308

Fix: viewport resized when switching sidebar tabs

The game layout had no fixed height, so the row sized itself to whichever sidebar panel happened to be tallest — switching to a shorter panel (like Hiscores with few bots) shrank the map along with it. The layout now has a fixed height on desktop (520px) with the sidebar scrolling internally instead of resizing the row; on narrow screens, the stacked map and sidebar each get their own fixed height instead of sharing one.

v1.8.307

Grind tab: one big game screen

Reworked from a scroll of separately-boxed sections into an actual game layout: a persistent viewport on the left (Slayer Master dialogue + the map, always visible) and a docked sidebar on the right with an OSRS-client-style tab strip — Bots, Skills, Slayer, Trade, Hiscores — switching which panel shows without ever hiding the map. Matches how a real game screen is laid out rather than a settings page.

v1.8.306

Smart bots, Grand Exchange, real OSRS icons

A cooking bot that runs out of raw fish no longer just sits idle — it intelligently switches itself to Fishing until restocked, then goes back to Cooking on its own (with a note on its card while it's doing this). New Grand Exchange section: sell surplus ore/logs/raw fish/cooked food for gp, or buy what you're short of, at a standard buy-above-sell spread — gp is a new currency, separate from credits and Slayer points, finally giving the resource stockpiles a real purpose. Resource cards, the Grand Exchange, and the map's resource nodes now show actual hotlinked OSRS wiki item/object images instead of generic icons.

v1.8.305

Grind tab: toast notifications for things worth knowing

Bot level-ups were completely silent before — now a toast names the bot, activity and new level (or a summary toast if several level up in one offline catch-up). A cooking bot meaningfully starved of raw fish gets flagged once per dry spell rather than staying silently idle. Crossing into "you can afford a bot" territory now says so. And finishing a Slayer task announces what the next one is, instead of leaving it a mystery until you next open the tab. All of these fire regardless of which tab you're on.

v1.8.304

Real-data Slayer sizing, skill tree, bot intelligence

Slayer task sizes are now calibrated off your actual history — total occurrences of a brand divided by days you saw any brand-matched work, aimed at roughly half a day's work, instead of an arbitrary fraction of the lifetime total. The flat repeatable "buy yield upgrade" is replaced by an actual skill tree: six one-time nodes across three tiers (yield boosts, cheaper bots, bigger inventories, a capstone), some gated behind earlier ones — a real path to choose, not one button mashed forever. Also: every bot now rolls a fixed "intelligence" (85-115%) at creation, a permanent yield multiplier giving bot-to-bot personality beyond pure activity level — visible in the click-to-inspect detail panel.

v1.8.303

Hiscores: show what each bot is up to

Both hiscore tabs now show an activity column — icon plus label — for each bot's current assignment, so the leaderboard doubles as an at-a-glance status board. Hidden on narrow screens to keep rank/name/value fitting comfortably.

v1.8.302

Lumbridge-style bot map + tutorial modal

The bot farm map now looks like a rough Lumbridge — a castle/bank courtyard, grass fields, a river along one edge, with fixed resource nodes (rocks, trees, a fishing spot, a cooking range) bots actually path toward instead of wandering aimlessly. Each bot shows a Material-icon badge for its current activity and a mini progress bar above its head; bots now cycle work → carry a small "inventory" → walk to the bank → deposit, purely as a visualisation layer (the real resource numbers stay entirely time-based, so offline progress still works correctly — this cycle never gates or drives them). Also added: a "How this works" tutorial modal on the Grind tab, explaining skills/credits/bots/Slayer/hiscores in plain language, shown once automatically and reopenable any time via the help button.

v1.8.300

Click-to-inspect bots + hiscores; economy retune

Bots are named from a curated list of iconic OSRS players now (Zezima, Woox, B0aty, etc.) instead of "Bot N". Click a bot — either its wandering square on the grid or its card — to open a detail panel showing all four of its skills (not just whichever it's currently assigned to) plus lifetime output. New Hiscores section with two ranked tabs: Total level and Lifetime output. Also: retuned the economy after real playtesting turned up a genuine "can't afford one bot" wall — first bot drops from 100 to 50 credits, and the credit-per-xp rate goes from 0.1 to 0.15.

v1.8.299

Fix: missed credits from before the bot farm existed

Real bug: the one-time xp-from-history backfill (v1.8.295) ran before credits/the bot farm existed (v1.8.297), so all that retroactively-earned xp never paid out its credit cut — leaving genuinely-earned levels but not enough credits to afford a single bot. One-time catch-up (`backfillCreditsFromXp`) now grants the missed credits based on total xp already on the books, guarded so it can't double-count for anyone whose backfill runs fresh going forward.

v1.8.298

Grind tab goes full game-mode

The Grind tab gets an explicit aesthetic waiver from the rest of the app's sharp/mono look — an OSRS-interface-inspired brown-and-gold reskin, scoped entirely to this one tab. Slayer tasks are now brand-level ("bring me 12 Canon items") instead of one exact model, weighted toward brands you actually inspect often. A Slayer Master NPC (portrait + dialogue box, OSRS chatbox-style) reacts to your current task. The bot farm gained a visual grid — each bot is a colour-coded square that wanders around on its own ambient tick, distinct from the economic 30s idle tick.

v1.8.297

Grind tab: idle bot farm + Slayer Master

New "Grind" tab next to Audit — the Skills section moved here from Strengths (which goes back to being pace-data-only), joined by an idle bot farm and a Slayer Master. Bots are individuals with their own OSRS-curve level per activity (Mining/Woodcutting/Fishing gather ore/logs/raw fish passively — including offline, computed from elapsed time on your next visit; Cooking processes raw fish into cooked food, capped by what you've actually got). Credits, a cut of every xp you earn, buy more bots. The Slayer Master assigns one task at a time targeting a specific item name — fuzzy-matched against what you actually inspect, weighted toward items you see often so tasks stay achievable — paying Slayer points on completion, spendable on an all-bots yield upgrade. Combat is deliberately not built yet. Hidden from the manager viewer, same as the rest of the Grind tab.

v1.8.296

Real OSRS cape art; substitute-item tracker fix

The level-99 "skill cape" badge now shows an actual OSRS cape image (hotlinked from the wiki — CSP's img-src extended to allow it) instead of a placeholder icon. Separately: substituting a wrong item (the "Wrong item" modal) after its old identity was already sent to the Wex tracker used to silently desync the tracker — the old category's count stayed wrong forever with nothing flagging it. Now, if the category actually changes, a reversal is queued for the old category and a fresh entry appears in the Audit panel for the corrected one; if the category is unchanged, it's just renamed in place since the sent count was already right.

v1.8.295

Gamification: OSRS-style skill XP and levels

The old "no gamification" rule is reversed — OSRS is Danny's favourite MMORPG, and it shows. Each item category (Lenses, Bodies, Lighting, Support, Audio, Bags & cases, Accessories, Other) is now a skill levelling 1–99 off the real Old School RuneScape xp curve, earning xp per item phase-tick (missing items earn nothing — no work was done). Badges unlock every 10 levels plus a level-99 "skill cape". A small "+N XP" drop appears near a ticked checkbox; an actual level-up gets the bigger treatment — the existing confetti burst plus a toast. Catching a wrong item via the substitute-item modal now also earns a flat attentiveness bonus. Retroactively backfilled from existing history so levels reflect real past work, not a fresh start. Lives on the Strengths tab; deliberately hidden from the manager viewer — this is a personal layer, not something the dashboard shows.

v1.8.294

Remove Texley

The mascot's gone — texley.js and texley.css deleted, their tags pulled from index.html, the Companion settings toggle removed, and every window.Texley hook cleaned out of app.js (quote-finished, day-archived, lunch start/end, boot mirror).

v1.8.293

Item rows: retrospective "harder than expected" flag

Came out of the NOVA stylebot styling sessions — a look at DAILYALLOC's own item-effort modeling surfaced that the `nova` checkbox's flat 45% weight can't distinguish a routine item from one with a heavy accessory list or extra faff. New optional flag icon appears on an item row once it's fully done: tap cycles harder → much harder → clear. Purely retrospective — doesn't touch live pacing, drift, or category-guessing math. Strengths tab's per-category bars now show a "flagged harder-than-expected" line once a category has 5+ resolved runs.

v1.8.292

Custom decline reasons + backfill from past "Other" notes

Settings gets a new tab to manage custom decline reasons, merged into the decline modal's dropdown ahead of "Other". Also scans today's quotes and archived history for notes already typed under "Other" so they can be promoted to a proper reason with one click instead of retyping.

v1.8.291

Activity log: item-checkbox clicks now carry the item id

First real recording session (v1.8.290) surfaced a gap in its own descriptor logic: a `.qcheck` checkbox's dataset only carries `phase` ("inspect"/"nova"/"admin"), so every item's admin-tick in a quote looked identical in the export — four admin clicks in two seconds read as ambiguous between "one item, four rage-clicks" and "four different items, ticked normally." Item rows already carry `data-iid` on their `<li>` wrapper; the descriptor builder now walks up for that ancestor the same way it already does for quote id, so click lines read `iid=<id>` and are traceable to a specific item.

v1.8.290

Activity log: raw clicks become an opt-in "Record session", not always-on

Reworks v1.8.289 after a same-day rethink: logging every click all the time meant the rolling buffer rotated away a morning's data within hours, and the result was mostly noise the semantic tags already covered. Tab switches, archive, quote finish, tracker sends, and undo stay logged always (cheap, rare, high-signal). Raw click capture is now a deliberate "Record session" toggle in Settings → Storage — start it, work normally, stop it (or it auto-stops after 60 minutes), then export. The live modal shows elapsed recording time.

v1.8.289

Activity log — local usage tracking for gap analysis

Always-on local activity log: every click plus a handful of semantic action tags (tab switches, archive, quote finish, tracker sends, undos) are recorded to their own localStorage key, capped at a rolling 8,000 events. Never synced to Firestore, never sent anywhere automatically. Settings → Storage gains "Export activity log" (downloads a plain-text file, one line per event, ready to paste into a Claude conversation to spot friction points and workflow gaps in real usage) and "Clear activity log". Superseded same-day by v1.8.290 — see above.

v1.8.288

Phrases pane: search + faster add; house-style sensor debris notes

Sidebar Phrases pane gains a live search box that filters by group, variant, and phrase text without losing focus while typing — matching groups auto-expand, others collapse out of the way. The add form is split into separate Group (with autocomplete of existing groups) and Variant fields instead of one hand-typed "Group · Variant" label, with Enter advancing group → variant → text and Ctrl/Cmd+Enter submitting. Also: the Sensor phrase group is rewritten from loose fragments to full house-style decline/note sentences that always state aperture range, ISO, and image location (e.g. "Debris under the sensor filter which shows as a small mark in the centre of images from [f-stop] to [f-stop] at ISO 100."), and the Grading modal gains a second static table for sensor-debris grade cutoffs (9− through Decline) alongside the shutter table.

v1.8.287

Stats: "Export for AI" button

Stats tab sub-tab row gains an "Export for AI" button. Copies your full archived history plus today's live day as JSON to the clipboard, ready to paste into a Claude conversation for pattern analysis — e.g. pulling out pacing trends, over-running quote categories, or missing-item counts before a manager 1:1. Only history/today quote data goes in the export; settings, sheet IDs, and OAuth tokens are never included.

v1.8.286

Texley right-click menu; per-day start-time override

Right-click Texley for a context menu with "Fuck off, Texley" — snoozes him for 24 hours (survives reloads, self-wakes if the tab stays open that long). Separately, the day-status right-click menu gains "Change today's start time…": set an actual late start and the end time shifts by the same amount, keeping the workday full length instead of shrinking it. Override applies to today only — clears at archive or on date rollover, and the effective window is stamped into the day's history entry.

v1.8.285

Camera DB: fix column offsets after shutter life deletion

Shutter life (column I) has been deleted from the source Google Sheet. The read parser and the sheet write array have both been updated to match the new column layout: I=SKU, J=release year, K=recall, L=things to look out for, M=common faults, N=manual URL. Write range updated from A:O to A:N. The old placeholder empty string for column I is removed.

v1.8.284

Shutter grading button on item rows

A speed-icon button now appears on each item row when the camera DB is loaded. Clicking it opens the shutter count grading modal pre-set to the body's tier (PRO or ENTRY from the camera DB) with the camera name shown in the modal header. If the item isn't matched in the camera DB it opens at PRO by default. Modal also updated to accept opts.tier and opts.cameraName.

v1.8.283

Grading modal: interactive shutter lookup; remove shutter life field

The grading / reference modal now has an interactive shutter count lookup: type an actuation count, toggle PRO or ENTRY, and the matching grade row highlights with the grade shown large in a colour that reflects severity (green through red). Stale "phrases moved to sidebar" hint removed. Shutter life (actuations) removed from the camera info modal, edit form, and sheet writes — the field is being removed from the source sheet.

v1.8.282

Item row: fix remove button wrapping to left on grid overflow

The item row layout was switched from CSS grid to flexbox. The grid had 8 fixed columns; when the camera DB is loaded the conditional camera-info button added a 9th child, causing the remove button to overflow and wrap to the start of a new row on the left side. Flexbox handles variable-width children without wrapping. The remove button also gains a small left margin to visually separate it from the other action buttons. On very narrow screens (≤420px), the camera-info and wrong-item buttons now hide alongside the duplicate button to keep the core controls visible.

v1.8.281

Camera DB: edit + missing-info prompt

Camera info modal now has an Edit button that opens an edit form for all fields (name, tier, type, mount, battery, charger, shutter count method and life, SKU, release year, recall flag, common faults, things to look out for, manual URL). Saving writes the updated row directly back to Danny's camera reference Google Sheet and updates the local cache. When battery, charger, or common faults are missing, a warning banner appears in the modal with a shortcut to fill them in.

v1.8.280

Wrong-item button: icon instead of text label

The "wrong item" button on each item row now shows a swap icon instead of the text label "wrong item", preventing the action cluster from wrapping on narrower items. Added swap_horiz to the icon allowlist.

v1.8.279

Wrong-item substitution + camera info modal

Each item in the day allocation now has a "wrong item" button: tap it to note that the customer sent a different item to what was quoted, search the product lookup or camera DB for the actual item, and save the substitution. The original name is shown as a "sub" chip so the record is clear. A camera icon button (shown when the camera DB is synced) opens a full-screen modal with all the camera's details — battery, charger, shutter count method and life, common faults, things to look out for, and a link to the manual. The sidebar camera cards also gain an "Full info" expand button to open the same modal. Both buttons are available directly from the item row without opening the sidebar.

v1.8.278

Remove Daily Allocation tab code

All code for the Daily Allocation Google Sheets tab has been removed. This includes: pull-from-tracker, row-order sync, row highlighting by item state, DA-tab rebuild, the morning-pull offer banner, the tomorrow-allocation indicator, and the item-by-item diff in the Audit view. The Adjustments tab, Allocation tab, and all related tracker write flows are unchanged. Audit tab now shows pending writes and the day-target section only.

v1.8.277

Camera DB: lookup pane + sheet ID persistence fix

New "Cameras" tab in the sidebar (rail icon + internal tab) backed by Danny's camera reference Google Sheet. Search by camera name or Wex catalogue name/SKU; results show battery, charger, shutter count method, rated shutter life, lens mount, tier, and recall flag. Data is cached locally and synced daily. Sync button available in Settings → Integrations. Also fixes a bug where saving any setting after a Firestore snapshot could silently push a null tracker/Product-Lookup sheet ID to the cloud, causing it to disappear on subsequent loads.

v1.8.276

Phrases: moisture, sensor marks, scroll dial, battery fixes

11 new quick phrases added across 5 groups: Moisture (optics light/heavy, viewfinder light/heavy), Scroll Dial (skips intermittently), Sensor (marks at F11, custom settings, top/bottom half), Battery (release mechanism), Accessory (Non-UK AC). Also fixes Battery · Fuji (full) which incorrectly read 0/4 instead of 0/5. All phrases land automatically on existing installs via the append migration.

v1.8.275

Phrases: faulty dioptre grading notes

Two new quick phrases added to the sidebar Phrases pane under the Dioptre group: one for cameras without Live View (dioptre restricted at neutral) and one for cameras with Live View (same restriction). Both are seeded into existing installs via a migration that appends any DEFAULT phrases not already present.

June 2026

v1.8.274

Stats: quote accuracy by duration tier; slow items use full history

The Items & quotes sub-tab in Stats now shows a "Quote accuracy by planned duration" table — all timed quotes grouped by their planned length (15m, 25m, 40m, etc.) with average actual time and pace per tier. More useful than the old per-QID actuals table which was always empty because every inspection job has a unique quote number. Slow-items correlation now scans all history instead of the last 14 days, and the minimum appearances threshold is lowered from 3 to 2, so patterns surface sooner.

v1.8.273

Fix: checkNovaTick crash — history shadowed by app variable

checkNovaTick called history.replaceState but the app declares its own let history (inspection history) which shadowed window.history. Fixed to use window.history.replaceState explicitly.

v1.8.272

Nova bookmarklet: auto-tick nova from the browser bookmark bar

Settings → Integrations now has a "Nova → tick" drag-to-bookmark link. After saving an inspection in Nova, click the bookmark — it opens a new app tab with ?nova-tick={qRef}, which auto-ticks the nova checkbox on all non-missing items in that quote and shows a toast. The original app tab picks up the change via a new cross-tab storage event listener, so it re-renders without a manual refresh. No extension needed.

v1.8.271

Nova extension + re-add 9m→8m tier correction

New Chrome extension (nova-extension/) bridges Nova inspection saves to Day Allocation. When Nova redirects to its "saved successfully" URL the extension reads the quote reference from the URL and auto-ticks the nova checkbox on the matching quote — no Nova page content is accessed, URL only. The app shows a confirming toast. Also re-adds 9→8 to the tier-correction table (incorrectly removed in v1.8.270).

v1.8.270

Fix: complete two-band tier-correction table

The Wex system produces two possible paste times per category. Added missing second-band mappings: 35→30 (Pro Cameras), 40→34 (Gimbals), 45→42 (Pro Video).

v1.8.269

Fix: tier correction handles non-zero seconds in paste times

When Wex's allocation export included seconds (e.g. 0:28:30), parseDuration was rounding up to 29 minutes — missing the PASTE_CORRECTIONS lookup and leaving the item uncorrected. Fixed by truncating seconds instead of rounding, matching the behaviour of parseSheetTime (which already did this correctly for the auto-pull path).

v1.8.268

Fix: audit item diff suppresses tier-correction false positives

The item-by-item audit comparison (diffAppVsTracker) flagged every tier-corrected item as "minutes-diff" when the Daily Allocation tab still held the original paste times. For example: a body pasted at 28m (corrected to 24m in the app) would always show "app 24m · tracker 28m" until the DA tab synced. The fix: when the tracker's time exactly matches item.allocatedMinutes (the pre-correction original), the difference is suppressed — it's expected, not a real discrepancy. Also removed a redundant dead-code branch in computeDayStatus where the in-progress and not-started quote cases had identical logic.

v1.8.267

Fix: three Material Symbols icons missing from allowlist

add_circle, remove_circle, and help were used in the app (EOD recap changes list, day-target audit "missing" row) but absent from the icon_names= allowlist in index.html — so they rendered as blank squares. All three added. The notify-manager message for app-only quotes ("isn't in the tracker's Daily Allocation tab") was also updated to remove the stale tab reference.

v1.8.266

Audit: category breakdown now includes Adjustments tab in tracker total

The Category breakdown comparison was only reading the Allocation tab's B–M columns, which hold the original allocation counts. Items sent to the Adjustments tab (+1 for added lines, −1 for missing) were ignored, causing false mismatches for any mid-day changes that had already been sent to the tracker. The tracker column now shows Allocation + Adjustments added − Adjustments missing per category, matching what the tracker actually reflects. When adjustments are present, a small incl. adj badge appears in the section header (hover for the breakdown).

v1.8.265

Audit tab: full UX overhaul — card layout, diff badges, jump button

Pending tracker writes now show as grouped cards per quote (QID group header makes it clear which write belongs to which job). The old cramped 8-column grid is gone — each card has a top row (sign, name, minutes, type chip) and a separate action row (full-width category select, Send, status). The minutes field is now editable on each card, so you can correct an auto-converted time before sending; changing it re-guesses the category automatically. Audit diff rows gained coloured badges ("app" / "tracker" / "time") so mismatches are scannable at a glance, plus a compact app-vs-tracker count chip above the list. Every audit row with a matching quote on Today has a new jump button () that switches to the Today tab and scrolls to that quote. Two new icons added to the Material Symbols allowlist: arrow_forward, receipt_long.

v1.8.264

Audit: category breakdown redesigned as compact grid table

The per-category comparison in the Audit tab is now a compact grid table rather than a verbose row-per-category list. Four fixed columns — Category / App / Tracker / status icon — keep all 12 categories scannable at a glance. Mismatches show an inline ±N delta next to the tracker count and an amber left-border highlight; matching rows show a green tick and stay visually quiet.

v1.8.263

Fix: sheet IDs now sync via Firestore and persist across devices

The Wex tracker sheet ID and the Product Lookup sheet ID were stored in localStorage only — so they vanished on any other device, browser, or after clearing storage, requiring you to re-enter them each time. Both IDs are now part of the synced settings object (Firestore), so once you save them on any device they stay everywhere. Existing localStorage values are migrated automatically on first load — no re-entry needed. The localStorage copy is kept as a fast-path cache.

v1.8.262

Audit: per-category count comparison against the Allocation tab

The Audit tab's day-target check now includes a Category breakdown section comparing your app's item counts per Wex category (Pro Cameras, All Cameras, Entry Lens, etc.) against the corresponding B–M columns in the tracker's Allocation tab. Each category shows app count vs tracker count with match/mismatch status; only categories where at least one side is non-zero appear. Handy now that the allocation comes in via Teams rather than through the Daily Allocation tab. The "no Allocation row" fallback message was also updated to not mention Daily Allocation.

v1.8.261

Parser: 4-column export format, tier time correction, no-time flag

The allocation paste now handles the 4-column export format (QID / Name / HH:MM:SS / Date) — the trailing date column is stripped automatically. Times from the paste are auto-corrected to your service tier: a camera at 28:00 in the export becomes 24:00 for you, a 19-minute lens becomes 16:00, and so on — with a small indicator on items where the time was adjusted. Items pasted with 0:00:00 (no time set in the allocation) are accepted rather than skipped, and show a ! warning in amber so you can spot them and set a time before starting.

v1.8.260

Unstart a quote, plus a code-review hardening pass

New "Unstart" action on the quote context menu: if you start a quote by accident, this drops it back to "not started" — clearing the start time and any pause history. Ticked items stay ticked; if you'd already done real work it asks first, since unstarting discards that quote's elapsed-time accounting. The inverse of "Reopen" (which un-finishes a completed quote).

Bundled with a round of fixes from a full code review: the service worker was pre-caching the wrong texley version, so the mascot couldn't load offline — now fixed. The cloud-sync listener no longer overwrites an edit you've just made if a remote update lands in the same instant (it keeps your local change and re-syncs). And tracker writes that hit a concurrent-edit "drift" during verify are no longer marked as sent — they stay in the Audit panel flagged "verify in tracker" so an adjustment can't silently vanish. Also: copy-to-clipboard chips (quote number, duration, time window) are now keyboard- and screen-reader-accessible, and some internal dead code / magic numbers were tidied up.

v1.8.259

EOD spillover badges now read against your actual pace

The red "EOD +45m" badges on later quotes were a red herring. They measured each quote's projected finish against the static schedule — which assumes every still-to-come quote takes its full planned time — so they stayed scary-red even on mornings you were comfortably ahead. They now grade against your live pace: the badge shifts the projected end by the day's current drift, then colours by where you'll actually land. Red only when you'll genuinely miss end-of-day at this pace; amber when it's tight; and a green "EOD -12m" margin when your pace already covers the overrun. The schedule worst-case is still there — it moved to the badge's hover tooltip. The pace credit is dampened (banks ~60% when you're ahead, full penalty when behind) so one fast quote doesn't flip everything green, and so the badge never under-warns when you're slipping.

v1.8.258

Fix: History per-day Timeline opens on collapsed days again

The "Timeline" button on a History day did nothing — it appeared to stop working after late May. Cause: History days default to collapsed, and the collapse styling hid the timeline panel with display:none !important, which the toggle's inline "show" couldn't override. So the only days whose timeline opened were ones you'd previously expanded by hand (the older ones). The toggle now uses a dedicated class with a more-specific rule that wins over the collapse, so the timeline opens on any day — collapsed or not — without having to expand the whole card first. The timeline data was always there (your recent days have 27–41 logged points each); it just couldn't be shown.

v1.8.257

Tracker automation: auto-send adjustments on archive (opt-in)

New Settings → Integrations toggle, off by default. When on, archiving the day first sends every pending Adjustments-tab write that already has a category to the Wex tracker — so you don't have to remember to hit "Send all" before wrapping up. Uncategorised rows are skipped and stay in the Audit panel for you to handle. The send runs before the archive snapshot, so the day records them correctly as sent; any row that fails is logged, left pending, and never blocks the archive. Stays off unless you turn it on — the manual review-before-send is still the default safety step.

v1.8.256

Tracker automations: morning-pull offer + auto-staged tomorrow

Two steps closer to the tracker driving itself. (1) Morning-pull offer: when the app is empty for today but the Daily Allocation tab already has today's block, the Today view shows a one-tap "Tracker has N quotes allocated for today — Load from tracker" banner. This replaces the old silent auto-load — the pull is now visible and confirmed rather than happening invisibly. (2) Auto-staged tomorrow: when mhall pastes tomorrow into the tab, the app now stages it straight into the next-day queue (the same slot the manual "queue tomorrow" box uses), so it loads automatically when the day rolls over — no re-paste. A manually-queued tomorrow always wins; only an empty or previously auto-staged slot is touched.

v1.8.255

Highlight-clear confirmed fixed — diagnostics removed

The lingering blank-row colours are gone. The fix that landed it was clearing both the legacy backgroundColor and the newer backgroundColorStyle (v1.8.253) — the tab had no banding or conditional-format rules, so once both fields were cleared the colour went. This release strips the temporary [DA-clear]/[DA-fmt] console logging back out, leaving just a quiet warning if a clear ever fails.

v1.8.254

Diagnostic: read the tab's conditional-format / banding rules

The v1.8.253 clear returned ok 200 over the whole column and the colour still didn't move — which means it isn't a cell background at all, but a separate formatting layer (a conditional-format rule or row banding on the tab). This release logs [DA-fmt] on Rebuild with the tab's actual bandedRanges and conditionalFormats, so we can see exactly what's painting those rows and remove it at the right layer.

v1.8.253

Highlight-clear: diagnostics + clear both colour fields

The blank-row colours persisted through three fixes with nothing in the console, so this adds visibility and widens the net. The clear now sets both the legacy backgroundColor and the newer backgroundColorStyle (whichever the sheet renders from), and logs [DA-clear] lines on every Rebuild — whether it ran, the row range, and the API status — so if it's still stuck we can see exactly why instead of guessing.

v1.8.252

Fix: highlight-clear now wipes to the end of the tab (the real fix)

Second follow-up — v1.8.251's clamp couldn't take effect because the tab's row count was never read: the sheet id had already been cached earlier in the session, so the lookup short-circuited before fetching the grid height, leaving the clear unbounded and still failing. Two fixes: (1) the lookup now re-fetches once if the row count is missing, and (2) the rebuild's "white everything" now omits the bottom bound entirely — Google reads that as "to the last row of the grid", so there's nothing to exceed and no row count to guess. Stale colours below the live content are wiped on the next "Rebuild & sort tab".

v1.8.251

Fix: the highlight-clear was silently failing on a trimmed sheet

Follow-up to v1.8.250 — blank rows further down the tab (e.g. 34–41, 47–57) were still coloured after a rebuild. Cause: the "white the backgrounds" call asked for a fixed 200-row range, but the Daily Allocation tab has fewer grid rows than that, so Google rejected the whole request (range exceeds grid limits) and nothing got cleared. Now the clear reads the tab's real row count and clamps to it, and the rebuild whites the entire tab rather than a guessed range — so stale colours anywhere below the live content are wiped. Run "Rebuild & sort tab" once on this version to clear what's currently stuck.

v1.8.250

Fix: Daily Allocation tab no longer leaves coloured blank rows after a rebuild

The duplicate-paste fix removed the repeated rows, but the highlight colours lingered on the now-blank cells — because clearing a cell's value doesn't clear its background formatting. "Rebuild & sort tab" now whites the whole A–C region's backgrounds before re-painting just the live rows, so stale colours (including ones left by a previous session, which the in-memory tracker couldn't see) are wiped. Archiving a day clears its block's formatting too, not just its values. Run "Rebuild & sort tab" once to clear any colours currently stuck on blank rows.

v1.8.249

Tomorrow-allocation indicator + archive clears the day's DA-tab block

Two tracker conveniences. (1) "Tomorrow" indicator: a strip on the Today view (and a line in the post-archive recap) reads the Daily Allocation tab and shows "Tomorrow: N quotes allocated in the tracker" or "Tomorrow: nothing allocated in the tracker yet" — so you can see at a glance whether mhall has pasted the next day. (2) Archive clears the day: archiving now removes that day's DAY: block from the Daily Allocation tab (surgically — other blocks, like tomorrow's, are untouched), so the finished day doesn't linger. Both are date-anchored and build on the v1.8.243 structure; run "Rebuild & sort tab" once so the headers exist.

v1.8.248

Fix: no more empty days to archive-and-delete in the morning

The "archive an empty day then delete it" morning chore is gone — and it was also blanking your History timelines. After you archive, the day resets and auto-loads the daily template (morning meeting blocks). The next morning, the stale-day rescue saw those template blocks as "content" and auto-archived the empty day, creating empty history entries (which render no per-day timeline and clutter the heatmap). Now: a day counts as archivable only if it has a real work quote or a done/started meeting — a bare template doesn't. The morning rescue discards an empty/template-only stale day and starts today fresh instead of archiving it, and manually archiving an empty day just says "nothing to archive." Existing empty entries can be deleted from History as before; no new ones will appear.

v1.8.247

Fix: archive recap "% of target" now matches the History tab

The post-archive recap card showed a different percentage than the History tab for the same day (e.g. 124% vs 103%). The recap used doneMin, which includes authorised untracked time (meetings, parcels, IT), while History uses computeDayTargetMetrics — inspection-time only (the manager's 7h goal is an inspection target; untracked time is logged for honesty but doesn't move the bar, v1.8.222). The recap now uses the same metric, so the number and the "Xh / Yh" line match History exactly.

v1.8.246

Re-push is now count-based — recovers partially-landed quotes too

Re-push now compares the app's line count against the tracker's count per category and re-sends exactly the difference, instead of going by quote-number presence. So it recovers a quote whose qid is already on the tracker but whose count is short — e.g. two lines on one quote where only one landed (the case that left a count of 2 instead of 3). It sends the tracker's missing quote-numbers first (to complete the qid cell), and only ever adds up to the shortfall, so it still can't double-count. This supersedes the qid-presence approach and closes the partial-landing edge noted in v1.8.245.

v1.8.245

Fix: Re-push under-counted two line-items on the same quote

The v1.8.244 re-push plan deduped by quote number, so two line-items on the same quote in the same category collapsed into a single re-send — three added lines pushed as a count of two. The Adjustments count cell is per line (the quote-number cell dedupes itself), so the re-push no longer dedups: each line re-sends. (Known edge: if a quote partially landed — its qid is already on the tracker but its count is short — the re-push still skips it, since qid-presence can't tell how many of its lines made it. Rare; flag it if you hit it and I'll add a count-based reconcile.)

v1.8.244

"Re-push adjustments" button — recover manual changes the tracker is missing

For when a send didn't land (e.g. the token expired mid-send): a new "Re-push adjustments" button re-sends the manual adjustments the app recorded as sent but whose qid is missing from the tracker's Adjustments block. It reconciles first — anything whose qid is already present is skipped — because the send path always increments the count (not idempotent), so a blind re-send would double-count. Shows a preview (how many, by category) before sending. Lives next to "Pull from tracker" / "Rebuild & sort tab".

v1.8.243

Daily Allocation tab: date-anchored blocks + "Rebuild & sort" button

The DA tab is no longer positional. It used to treat "the top block" as today, which broke whenever mhall pasted the next day below and the old day lingered. Now every day's allocation sits under a DAY: YYYY-MM-DD header row in column A, and the app finds today by date — deterministic, no more wrong-block reads. New "Rebuild & sort tab" button (next to "Pull from tracker"): it shows a dry-run preview (today's rows from the app, future blocks it'll keep, past/leftover rows it'll remove) and only on confirm does it wipe & rewrite the tab into clean dated blocks — including a labelled "PASTE TOMORROW'S ALLOCATION BELOW" banner so mhall has one obvious spot. Past-dated blocks drop automatically, so no more manual morning deletion. The read path falls back to the old positional scan on any tab that doesn't have headers yet, so nothing breaks before the first rebuild. (ISO dates in the header sidestep the 06/01-vs-01/06 ambiguity entirely.)

v1.8.242

Fix: manually-added items no longer duplicate at the bottom of the Daily Allocation tab

Groundwork for the Daily Allocation tab rewrite. When you added an item and sent it, the app appended the line to the DA tab via the Sheets append API — which drops it at the bottom of the whole A:C range, i.e. after mhall's next-day block. Meanwhile order-sync rewrites today's full block (added item included) at the top. Net result with order-sync on: the added item appeared twice, and its row-highlight scattered to the orphan at the bottom. Now the append only runs as a fallback when order-sync is off; with order-sync on (the default) the reorder is the single source of truth, so no orphan and no split highlights. Prevents new duplicates — a follow-up release rebuilds the tab into clean, date-anchored blocks.

v1.8.241

Fix: tracker date matching no longer reads the wrong row (06/01 vs 01/06)

The Audit's "Day target check" was reading zeros — and it was a date-locale collision, not a missing sync. Both tracker lookups (the Allocation-tab day-target row and the Adjustments-tab block) built six candidate date strings — DD/MM/YYYY, MM/DD/YYYY, etc. — and returned whichever row matched any of them first. But 06/01/2026 (US for 1 June) is also a valid UK date — 6 January — so on a UK workbook an earlier "6 Jan" row stole today's match and the audit read an empty row (Total lines 0, time 0), making it look like nothing had synced. Replaced both with one shared matcher that infers the workbook's day/month order from an unambiguous date in the column (any component > 12) and matches today in that order, defaulting to UK day-first, then ISO, then US — so the ambiguous case can't grab the wrong row. Also widened the app's connect-src CSP to the script CDNs so Firebase/library source-map fetches stop erroring in the console (dev-only noise; no functional effect).

v1.8.240

Fix: Google Sheets sync now prompts to reconnect instead of silently dying

The background token refresh can't work anymore, and the app wasn't telling you. Google Identity Services' token popup requires a real user click, and Chrome's removal of third-party cookies killed the old silent-iframe refresh path. So the timer that tried to silently renew your Sheets token ~5 min before expiry was just firing a popup the browser blocked ("Failed to open popup window"), the watchdog fired, and — on a mid-session expiry — nothing surfaced. Sheets sync would quietly stop working until you happened to retry an action. Now a silent-refresh failure (watchdog timeout or error) drops the stale token and shows the existing one-click "Reconnect" toast, whose button opens the consent popup inside a real gesture (which the browser allows). The prompt also re-arms each token cycle, so a long day re-prompts as needed. The boot-time reconnect prompt was already working; this brings the in-session path in line. No change to the actual send/audit logic.

v1.8.239

Privacy & Terms links in the footer

Added Privacy and Terms links to the login footer, pointing to the new estate-wide policy at dannylaw.dev/privacy.html and /terms.html. Part of a wider privacy/security pass across all the dannylaw.dev sites: the policy is an honest, plain-language account of what each site does and doesn't collect (this app stores your account + inspection data in Firestore and, if you connect it, your Google Sheet; no analytics, no tracking, no first-party cookies anywhere). The spares page got the same footer links.

v1.8.238

Today's target is now an obvious, tappable control

Changing today's daily target (the Workload strip's "/ 7:00") was only discoverable if you happened to notice the faint dotted underline and guessed it was clickable. Added a small pencil icon next to the target so it reads as an editable control at a glance — tap it (or the target text) to open the same "Today's target" dialog, which overrides the target for today only and clears at archive. No change to the underlying behaviour or the global default in Settings → Workday.

May 2026

v1.8.237

Tracker date helpers fail loud on a bad argument

Defensive hardening, no user-facing behaviour change. findTrackerDateBlockRow and fetchAllocationRowForDate both did date instanceof Date ? date : new Date() — so passing anything that wasn't a Date (e.g. a state.date string like "2026-05-29") silently fell back to today. Every current caller passes a real Date, so nothing was broken, but a future caller auditing a past day would have read the wrong row with no error. Both now throw on a non-Date argument; calling with no argument still means "today" for the live-send path. Also confirmed during review that order-sync's write-back preserves item names verbatim (parseInput never renames — it only attaches SKU / fuzzy flags), so no name drift into the shared tab.

v1.8.236

Fix: Allocation-tab day-target check is now locale-tolerant

fetchAllocationRowForDate only matched DD/MM/YYYY. The live Audit's day-target check (Total Lines / Time Allocation / Time Target / % of Target) looks up the day's row in the tracker's Allocation tab by formatting today's date one way — en-GB. But the Adjustments write path (findTrackerDateBlockRow) has tried six date formats since v1.8.170. So if the shared workbook's locale ever flipped (US MM/DD/YYYY, dash or dot separators), tracker sends kept working while the day-target check silently returned "no row for today" — the two halves of the audit disagreeing with no visible cause. The Allocation lookup now tries the same six formats, and reports back the actual matched cell string rather than the assumed one.

v1.8.235

Fix: DA-tab order-sync no longer wipes a pre-pasted second chunk

Hardening on the write-back path to the tracker's Daily Allocation tab.

Order-sync's trailing-row clear could delete tomorrow's allocation. When you FINISH/reorder/archive, syncDailyAllocationOrder rewrites today's block to A2:C{N+1} then clears the rows below to remove leftovers from a longer prior write (the v1.8.231 duplicate fix). It cleared a blind 200-row buffer — but the read side has known since v1.8.190 that the DA tab can hold tomorrow's allocation pre-pasted below today's, separated by blank rows. So the first FINISH of the day could silently wipe that second chunk from the shared workbook. The clear is now chunk-aware: it probes column A:B downward and stops at the first blank row, clearing only today's own leftover run. If the probe read fails it falls back to clearing only our own prior footprint — never blind rows below.

Highlight flush is now re-entrant-safe. Three callers race for flushDaHighlightQueue (the 12s scheduled timer, the immediate FINISH/archive flush, and the order-sync repaint). Without a guard two could both pass the empty-check, both await the sheet-id lookup, and the loser would drain an already-emptied queue and POST a no-op batchUpdate. Added an in-flight guard mirroring the order-sync one. Also tidied the flush's network-error branch to clear its backoff stamp and toast on exhaustion, matching the HTTP-error branch.

v1.8.234

Fix: target-override leak + Teams recipient popover z-index

Two bug fixes flagged in the wild.

EOD modal (and four other surfaces) ignored today's target override. The EOD hero said "vs 7h target" even when today was overridden to 6h. Root cause: seven call sites passed a stripped { quotes: state.quotes } object to computeDayTargetMetrics, which then dropped through getEffectiveDailyTargetMin's priority chain (override → setting) and landed on the global settings.dailyTargetMin. Fixed by passing state directly (it already has the override field) at: the live progress bar in render(), the debug dump, the day-status strip, the EOD modal, buildEodRecapText, computeDayRecapStats (had to merge the fields since the helper takes quotes only), and the Stats hero. Today's override now reaches all seven.

"Open in Teams" picker appeared behind the preview modal. When a Notify-manager flow opened the Teams preview modal (z-index 9100) and the user clicked Open in Teams with multiple contacts configured, the recipient popover (z-index: 9000) showed up under the modal — looked like a "context menu behind the modal". Bumped .teams-recipient-popover z-index to 9200 so it sits above its parent modal.

v1.8.233

Smarter auto-sort + decline-prone item heads-ups

Two related learning improvements. The captured-but-unused history signals (per-item-name pace, time-of-day pace, decline rate) now actually steer scheduling and surface in the UI.

Auto-sort: three new signals layered on the existing per-qid pace ratio.

  • Per-item pace fallback. Qids rarely recur, so per-qid history rarely fires. When the qid lookup misses (or has <3 prior runs), the score now sums item.minutes × itemMedianRatio over the quote's items, taking the per-item median from history where there are ≥3 runs. Requires ≥2 items with history before swapping in the synthetic estimate, so a one-item match doesn't swing a whole quote on noisy data.
  • Time-of-day pace shift. After the initial sort, walks the queue in score order, projects each quote's start time, and multiplies its score by the start-bucket's median ratio. Clamped to [0.8, 1.2] so it nudges placement without overruling the per-item / per-qid pace signal.
  • Decline-rate downweight. Quotes containing decline-prone items (≥30% historical decline rate over ≥3 runs) get up to 10% off their effective duration, scaled by share of decline-prone items in the quote. They finish faster in practice because declined items skip inspect+nova.

Sort basis is now stamped on each quote (_sortBasis: 'qid' | 'item' | 'plan') so future debugging can see which signal fired. _sortRatio, _sortBucketShift, and _sortDeclineShare all preserved for the same reason.

Decline-prone chip across three render surfaces.

  • Today tab quote items. Small mono "decl X%" chip next to the item name. Tooltip says "Declined N of M times · usually mould" (or whichever decline reason is the mode for that item in history). Hidden once the item is itself declined or missing — point's already made by then.
  • Lookup view results. Compact version of the chip in the meta row, alongside mins / usage / catalogue chips. Warns before the user adds the item to a quote.
  • Parse-day toast. A new parse-toast-decline block lists up to 3 decline-prone item names from the freshly parsed paste with a count of how many in total. Toast extended from 7s to 9s so the user has time to read it.

All three surfaces and all three sort signals share one helper map (buildItemHistoryMaps) built lazily on first access and cached behind a history.length + history[0].date key. The cache invalidates automatically on archive (new entry at index 0) and on cloud-snapshot replacements (newest date changes), without needing explicit hooks at each call site.

v1.8.232

Right-click context menus across the Audit tab

Four new context menus on the Audit page, all built on the existing showCtxMenu() helper. Shift + right-click anywhere escapes to the native browser menu (same convention as quote/item cards).

  • Audit rows. Copy QID, Copy as Teams message, Notify manager (non-match only), Jump to quote on Today (greyed when the row is tracker-only since there's nothing in app state to jump to), Open in tracker (deep-links to the matching A{row}:C{row} range via the cached _daRow), Refresh.
  • Pending tracker writes. Copy QID, Copy item name, Send now (dispatches to the visible per-row Send button so the status pill and error toasts reuse), Discard, Jump to quote, Open in tracker, Refresh.
  • Day target check rows. Copy summary (a short text block with total lines / time allocation / time target / % of target), Open Allocation tab, Refresh.
  • Audit page background. Refresh, Open tracker, Help. Fires anywhere on the audit view that isn't claimed by a more-specific row.

Row-specific handlers stopPropagation() so the background-fallback handler doesn't double-fire. Pending-row actions resolve their index against the live pending list at click time, so a row that vanished between render and right-click bails cleanly. Renderer caches the diff into _lastAuditResults so audit-row menu actions get the latest payload without coupling render and menu code.

v1.8.231

Day-action rail icons + DA-tab duplication fix

Two changes in one release.

Rail promotion. Export, Shutter/grading table, and Archive day all moved out of the sidebar's Day pane onto the always-visible left rail. Export and Reference sit above Auto-sort; Archive is pinned to the bottom of the rail, below Settings — keeping the destructive action at thumb-distance from the safe ones (and the confirm modal still gates it). The Day pane and Day rail icon are gone, since they'd be empty. Same dispatch pattern as the v1.8.205 auto-sort move: rail buttons .click() hidden source nodes, so the original handlers don't need rebinding.

DA-tab duplication fix. When mhall pasted today's allocation directly into the tracker and the app then auto-sorted (FINISH, manual drag, or archive), the sorted block ended up sitting above the leftover paste rows — looking like a duplicate. Cause: the leftover-clear in syncDailyAllocationOrder only fired when _daOrderLastWrittenRows > rowSpecs.length. After a fresh paste we didn't write, that var was 0, so the clear was skipped. Fix: the clear now always runs with a generous 200-row trailing buffer below the new endRow, covering both first-sort-after-paste and any garbage rows below the chunk.

v1.8.230

Unlink admin from inspect/nova — no more auto-tick cascade

Ticking the admin checkbox on an item no longer auto-ticks inspect and nova for you. Each of the three phase boxes is independent now — you tick them as you actually do the work. The old behaviour (admin tick back-fills inspect+nova with synthetic timestamps for non-declined items) was convenient when admin was always the last step, but it muddied per-phase timing data and made it impossible to record "admin done before inspect/nova" sequences.

itemIsFullyDone still requires all three (or declined && admin), so admin alone won't mark an item complete — the FINISH button will still gate on a real full tick. Unticking inspect or nova still clears admin, since admin can't logically stand without the upstream phases.

v1.8.229

Security hardening — SRI on CDN scripts, pasteBox length cap

Two defensive additions from the full security audit. Most of the audit came back clean (Firestore rules tight, CSP strong, no committed secrets, escapeHtml consistent across 280 call sites on 191 innerHTML writes, OAuth tokens correctly scoped, service worker same-origin only).

  • SRI hashes on cdnjs and jsdelivr scripts. dayjs / Chart.js / Motion One previously loaded without integrity attributes because cdnjs's published SRI hashes have been wrong before (we hit that with Fuse v7). Computed SHA-384 hashes directly from the served bytes and added integrity attrs to index.html and pitch.html. If those CDN files ever change, the browser refuses to execute them — loud failure beats silently running tampered code. Firebase scripts (gstatic.com) and Google Identity Services stay un-hashed because they're served from Google's own identity infrastructure.
  • maxlength="100000" on the paste box. Hard cap on textarea input — 100KB is plenty for any realistic day's allocation and defensive against accidental huge pastes more than malicious ones.

The audit's three "known trade-offs" not addressed (OAuth tokens in localStorage, style-src 'unsafe-inline', broad img-src) are all practically required by the architecture — each addressable only by switching to a different backend or breaking integrations, neither warranted by the real risk.

v1.8.228

Fix: highlights now follow rows after an order-sync

v1.8.227's repaint-after-reorder went through flushDaHighlightNow + the snapshot loop, deferred 400ms. In practice that was either (a) too short for Sheets to commit the values.update before the batchUpdate landed, or (b) racing the periodic 5s snapshot. Result: rows shuffled in the sheet, but the row colours stayed pinned where they'd been before.

Rewrote the repaint inline: clear _daHighlightLastSynced + the queue, walk rowSpecs and queue colourForItem(item) at each item's new row directly (no snapshot intermediary), defer 1500ms (so values.update has time to commit), then flushDaHighlightQueue. No reliance on snapshot loop gates or timers — every row gets explicitly queued exactly once per order sync.

v1.8.227

Sync DA-tab row order to match the app's current quote order

mhall's view of the Daily Allocation tab now follows the order you actually work through. Triggered on FINISH, manual drag (any path through notifyManualReorder), and archive — 30s throttle inside scheduleDailyAllocationOrderSync coalesces rapid changes into a single write.

Implementation: each sync builds a fresh [qid, name, HH:MM:SS] payload from state.quotes (work quotes only), pushes it to 'Daily Allocation'!A2:C{N+1} via values.update, then clears any trailing rows if the previous content was longer. After the write succeeds, every item's _daRow is re-stamped to its new sheet row and _daHighlightLastSynced is cleared — because Sheets ties cell formatting to rows (not values), the colours need re-applying after a reorder or stale colours stick to whatever happens to land at each row. The repaint runs through the existing flushDaHighlightNow path (400ms delay so the values.update propagates first).

Archive specifically does {force: true} before the highlights-clear step, so the final morning view of the tab reflects the actual sequence the inspector worked through.

Toggle: Settings → Integrations → Sync row order to tracker. Default ON.

v1.8.226

DA-tab highlight polish — instant flush, periodic re-backfill, archive cleanup, backoff

Five tightenings to the DA-tab highlight loop:

  • Instant flush on FINISH / archive / missing / decline. New flushDaHighlightNow() runs a snapshot pass and drains the queue immediately, bypassing the 12s scheduled flush. Wired into userFinishQuote, archiveCurrentDay, the missing-toggle action, and the decline-modal save. Hitting FINISH on a quote now paints the tracker rows green within ~1s instead of up to 17s.
  • Periodic re-backfill (10 min) + on tab refocus. If mhall inserts or deletes a row mid-day, cached _daRow stamps go stale. backfillDaRowsFromTracker now re-runs every 10 min and whenever the tab regains focus after being hidden ≥5 min. Self-heals structural edits.
  • Clear all tracked highlights on archive. Before archiveCurrentDay clears state, every row in _daHighlightLastSynced gets queued white and immediately flushed. The DA tab starts tomorrow morning clean instead of carrying yesterday's stains.
  • Exponential backoff on flush failures. A 403/429 from Sheets used to put the queue into a 12s retry loop forever. Now: 12s → 60s → 300s, then drop the queue + toast "Tracker highlights paused — check Sheets permissions". _daFlushBackoffUntil guards the queue between retries.
  • Don't persist _daRow. Dropped the saveState() call in backfillDaRowsFromTracker. _daRow is a transient position cache — persisting it burned Firestore writes for derived data AND let stale stamps survive a reload. In-memory only now; backfill at boot + periodic re-runs handle re-stamping.
v1.8.225

DA-tab highlight backfill — existing manually-pasted items now link up too

v1.8.224 stamped _daRow on items when they came via the new pull/append flows. Items pasted manually before that (or pasted via the textarea today) had no row number, so they stayed un-highlighted even when their state changed.

New backfillDaRowsFromTracker() runs 4s after the highlight loop starts: fetches the DA tab once, matches each unstamped state item against the live tracker rows by (qid, name), and stamps _daRow. Idempotent — items that already have a row are skipped. After backfill the regular 5s snapshot picks up any state changes.

One reload after the deploy and today's already-ticked items should colour up within ~15-20s.

v1.8.224

Daily Allocation tab rows auto-highlight by item state

The DA tab in the tracker now reflects per-item status visually. As you tick items inspect/nova/admin, mark missing, or decline, the corresponding row's background colour updates:

  • Light green = fully done (all three checks ticked, or declined+admin)
  • Light lime = some progress (at least one check ticked)
  • Light amber = declined
  • Light red = missing
  • White = pending (cleared)

How it works. Each item carries a private _daRow field (sheet row number) stamped at three points: (1) when items are pulled from the DA tab via pullDailyAllocationToToday, the originally-fetched rows' row numbers get matched onto the parsed quotes by (qid, name); (2) when items are appended via appendDailyAllocationRows, the response's updatedRange is parsed and the new row number is stamped onto the matching live state item via itemId; (3) the DA tab's numeric sheetId (gid) is fetched once and cached so batchUpdate requests can target rows by index.

API throttling. A 5s snapshot loop walks state.quotes, computes the desired colour per item, and queues anything that's diverged from the last successfully-synced colour. A 12s flush timer then batches all queued changes into a single spreadsheets.batchUpdate with repeatCell requests. Failed flushes re-queue. No-op when toggled off, when not connected, when not today's date, or when in viewer mode.

Toggle in Settings → Integrations → Highlight Daily Allocation rows by state. Default ON. Turning off clears any pending queue so the tab stays as-is.

v1.8.223

Auto-pull today's allocation from the tracker · added items now also append to Daily Allocation

Pull from tracker. Two ways to load today's allocation directly from the Daily Allocation tab in the tracker sheet:

  • Auto on boot — fires 3s after boot when (a) state.date is today, (b) state has zero work quotes, and (c) Sheets is connected. Silently no-ops otherwise. Lets you open the app in the morning after mhall's pasted and see your day already loaded.
  • Sidebar button — new "Pull from tracker" button in the Input pane, right below Parse Day / Merge. Confirms before replacing if today already has content. Pops the auth flow inline if you're not connected (no Settings detour).

Both paths run the fetched rows through the same parseInput as a manual paste, so item IDs, library matching, scheduling, and defaults all behave identically.

Added items now also append to Daily Allocation. Previously, items you added in the app went only to the Adjustments tab (count + QID per category). The Daily Allocation tab stayed as mhall's snapshot, so the audit's per-quote diff flagged your additions as "app-only" forever. Now, after a successful Adjustments-tab send for an 'added' entry, the same line appends to the Daily Allocation tab (spreadsheets.values.append) — the literal row lives in the tracker, no more spurious mismatches. DA-tab append failure is non-blocking: the count-side send still landed, just logs the warning.

Caveats: append uses Sheets' default end-of-table position, so if the DA tab holds tomorrow's chunk below today's, the new row will land after tomorrow's chunk (find-end-of-today-chunk logic deferred until that pattern becomes the norm). Reversals don't currently delete the row — they only undo the Adjustments-tab count.

v1.8.222

Daily target % is inspection-only — untracked time no longer inflates it

Untracked time used to count toward the daily target % when marked authorised (default for any entry without an explicit authorised: false). Legacy untracked entries that never got the flag set default to authorised, which silently rolled "parcels and coffee" / IT / training time into the target numerator and inflated history numbers.

The manager's 7h goal is an inspection-time target, so the % math should match. Changed computeDayTargetMetrics's rawPct to use inspectionDoneMin / baseTarget only. All visible "X of Y target" labels (history bar tooltip, history tile, day-status sub-label, EOD recap heroline, EOD copy-to-clipboard text, status view) updated to display the matching inspection minutes — no more "(mixed)" annotation, no more X/Y ratio that disagrees with the headline %.

Untracked time is still tracked and stored — tooltips now report it as "logged but not counted" so the breakdown is transparent without affecting the headline.

v1.8.221

Wex-icon — rebuilt as filled blades on a dark backdrop (centre hole now visible)

The previous attempts at a centre hole kept failing because the construction was "lime disc + dark stroked cuts + tiny dark dot in the middle" — the dot was either swallowed by the converging stroke endpoints or separated from them by a thin lime ring, and at the icon's 14px display size neither was readable.

Rebuilt the SVG using the actual Wex logo construction: a single dark disc backdrop with 6 filled blade-shaped paths on top in currentColor. Now the gaps between blades expose the dark backdrop (= the cuts) and the area inside the blade tips not covered by any blade exposes the same backdrop (= the centre hole). One continuous dark shape behind everything, blades are the only colour-driven part, and the centre hole reads cleanly because it's the same paint as the cuts.

currentColor inheritance still works: blades pick up color: var(--ink-faint) by default and var(--accent) on hover from CSS — just like before, just inverted (blades change colour now, dark cuts stay constant).

v1.8.220

Wex-icon — visible centre hole, matches grey button-icon styling

Two fixes on the per-item wex-link icon.

  • Centre hole now reads. The previous version's blade-separator strokes converged all the way to (50, 50) and the tiny dark dot got swallowed by the stroke mass. Regenerated paths now stop at radius 7 (with a swirled inner endpoint), and the dark centre disc is now radius 6 — sits prominently as a punch-through hole exactly like the original brand mark.
  • Greys to match button-icon styling. Dropped the wrapper box (border + surface background); icon is now a bare inline-SVG with fill="currentColor" on the disc. CSS sets color: var(--ink-faint) by default and var(--accent) on hover — same pattern as the qid-link / qduration arrows. Reads as a native app affordance instead of a foreign-feeling box. Inline SVG (rather than <img>) is required because external SVG can't inherit currentColor.
v1.8.219

Recolour wex-icon to the app's lime palette

The Wex aperture sitting next to each SKU was still using the brand cyan (#00ceff), which fought DAILYALLOC's lime visual language wherever it appeared. Recoloured to lime disc (--accent) with dark cuts (--bg) so it reads as a native app affordance, not an embedded brand mark. Hover state on the icon's wrapper also switched from cyan to --accent-dim / --accent-glow.

v1.8.218

Item-line SKU now opens NOVA; wex-icon next to it opens wexphotovideo

Split the per-item SKU link into two destinations. The SKU pill itself now opens the matching NOVA product record (nova.wexphotovideo.com/Product?SKU=…) — NOVA is the inspection tool, so that's the link an inspector actually wants. The wexphotovideo public-site search lives behind a small Wex aperture icon sitting right after the SKU.

New public/wex-icon.svg — hand-drafted aperture mark (6 swirling blade-separators on a cyan disc) matching the Wex brand. novaUrlForSku() helper added alongside wexSearchUrlForSku() so the two destinations are centralised. Applies to both today's quote rows and past-day history rendering.

v1.8.217

"Now" button on the Add-meeting modal Start input

Small but constant quality-of-life on the Add-to-Schedule modal: a "Now" button sits next to the Start time field. Click it and the start stamps to the current wall-clock (no 15-min rounding — Now means now). Dispatches a change event after setting so the "Until end of workday" auto-fill still re-derives the duration if that box is ticked.

v1.8.216

Add SKU context menu + clickable SKU → wexphotovideo search

Two small but high-utility additions to quote item rows.

  • "Add SKU…" / "Edit SKU…" in the item context menu. Items pasted in without a library SKU match (or with a fuzzy match you want to correct) can now have a SKU added manually. The new modal sanitises the input the same way migrateState does on load (trim, drop control chars, cap at 64 chars) and clears the libraryMatchFuzzy flag since manual entry is authoritative. Has a Clear button when an existing SKU is present.
  • Clickable SKU → wexphotovideo search. The SKU pill on every item is now an anchor that opens https://www.wexphotovideo.com/search/?q=<SKU> in a new tab. Hover swaps to the lime accent so it's discoverable. Applies on both today's quote rows and past-day history rows. Tooltip explains the click target.
v1.8.215

Google Sheets — auto-silent-refresh at boot + auto-popup on action

Closer to "it just works" for the Sheets sign-in flow. Two changes:

  • Boot auto-silent-refresh. If you've previously connected (we have your sheet ID stored) but the token's missing or expired, the app now fires a silent token request automatically on boot — uses a hidden iframe against your existing Google session, no clicks. Works whenever Chrome's still signed in to the same Google account that owns the tracker. Falls back to a sticky reconnect toast only when the silent attempt fails (browser-blocked cookies, session revoked, consent withdrawn).
  • Auto-popup on action. New ensureSheetsConnected(callback) helper. When you click an audit "Send" or the Connect button on the audit empty-state, if there's no valid token the auth popup opens immediately from within your click (browsers require a user gesture for popups — this satisfies it). On successful auth, the queued callback fires automatically, so the original action just completes. No "Connect first → go back → retry" maze.

What this means in practice: open the app in the morning, and if you'd already connected the day before, Sheets is just connected. If overnight the token died for a reason silent refresh can't recover from, a small toast in the bottom-right with a Reconnect button is one click away. And if you go to Audit while disconnected, the Connect button on the empty-state pops the auth flow right there instead of bouncing you to Settings.

v1.8.214

Texley — wind-resistance on tie + antenna

Tie and antenna now lag behind the body's motion like loose-attached parts catching wind. Both groups got transform-origin pivots (tie at top where it joins the body; antenna at base where it meets the head) and a CSS-variable-driven rotate (--tie-tilt / --antenna-tilt).

During drag, pointermove computes EMA-smoothed pointer velocity (raw pointermove rate is jittery, so 60/40 EMA dampens it). Velocity → rotation in the OPPOSITE direction of motion (they trail), capped at ±22° for the tie and ±30° for the antenna (more reactive, lighter mass). Transitions during drag are 80ms linear so the lag tracks velocity tightly. On release: tilts reset to 0 with the elastic-overshoot transition (cubic-bezier(0.34, 1.5, 0.64, 1)), so they swing forward past zero and settle.

Same physics applied at gentler scale during wander — body leans into motion, tie + antenna trail behind, all reset on arrival with the same swing-back. Reduced-motion users skip the lot.

v1.8.213

Texley — eye-tracking, motion physics, context awareness, Wexley refs, edge-aware bubble

Big pass on Texley to make him feel alive.

  • Pupils that follow the cursor. Each eye now has a dark pupil. mousemove → rAF-throttled handler computes the eye-to-cursor vector and translates each pupil along it, capped inside the eye-radius. Damped follow with a 160ms cubic easing transition so it feels watchful, not jittery. Eye-group blink still scales both eye and pupil together.
  • Context-aware speech. When Texley auto-speaks, he reads the live app state (active quote, target %, time-of-day) and picks from a contextual phrase pool with a 60% bias when context is available. Pools: empty-day, has-active-quote (mentions the QID), half-target, near-target, target-hit, morning, pre-lunch, afternoon, late-day, after-hours. Falls back to the generic Texan pool otherwise.
  • Wexley references. ~15% of auto-bubbles call out to his brother Wexley at /wexley ("My brother Wexley sends his regards", "Wexley's the original — I'm the new model in lime", etc.).
  • Edge-aware speech bubble. Bubble was clipping when Texley wandered to the top or left edges. pickBubbleAnchor() now computes viewport edge distances at speak time and picks one of four anchors (top-left / top-right / bottom-left / bottom-right); each variant is a CSS class on the overlay that flips the bubble position AND the tail direction. Bubble never gets cut off.
  • Motion physics on wander. SVG wrapped in a separate .texley-physics layer that handles motion transforms independently from the inner SVG's idle breathing. On wander, JS sets --motion-tilt in the direction of travel (capped at ±7°), and CSS transitions it over the 1.4s glide. On arrival, the .landed class triggers a 420ms squash-overshoot keyframe (28% scaleY 0.9 / scaleX 1.08 → 60% scaleY 1.04 / scaleX 0.98 → 100% rest). Drag-drop without going off-screen now also triggers the landing squash.
  • More idle animations. Added a slower double-blink layer (mixes with the regular blink so timing doesn't feel mechanical).
v1.8.212

Texley — DAILYALLOC mascot (recoloured Wexley, wanders, draggable, Texan-accent)

Meet Texley. A recolour of the Wexley character from the sibling /wexley project, dropped into DAILYALLOC's lime / ink palette. He sits in the bottom-right corner of the app, says Texan things ("Howdy partner.", "Mighty fine pace yer keepin'."), and reacts to app events (quote finished, day archived, lunch start, lunch end).

  • Idle animations — blink, breathe, periodic eyebrow twitch and mustache wiggle, antenna glow pulse. Honours prefers-reduced-motion.
  • Wandering — after 2 minutes of no user activity, he picks a random viewport position (biased toward edges) and glides there. Picks a new spot every 15s while idle. Any user input stops him.
  • Draggable — grab him and toss him toward any edge. Past the edge he says "See ya, partner." and hides for the session. Setting unchanged — reload brings him back. Or toggle the Settings checkbox off-then-on to re-summon.
  • Click cycles to the next phrase.
  • Settings → Behaviour → Companion · Show Texley — default ON. Untick to suppress entirely.

Files kept separate per request: all visuals + behaviour live in /public/texley.css and /public/texley.js. Removing the two <link> / <script> tags in index.html removes the feature entirely; no app.js references break. The hooks app.js does call (window.Texley.setVisible, window.Texley.notify) are guarded with if (window.Texley && ...) so they no-op cleanly when texley.js isn't loaded.

v1.8.211

Fix orphan "waiting on battery" banner on completed quotes

A completed quote was still rendering the qcharging-banner ("N items waiting on battery") because v1.8.203's auto-close was wired into userFinishQuote / maybeStampCompletion / archiveCurrentDay but missed two less-common completion paths: the inline timestamp pill edit and showEditQuoteTimesModal. Any quote that was finished via either of those still carried charging: true on its items — and any quote that completed before v1.8.203 shipped did too.

Three-part fix:

  • Banner hidden on completed quotesq.completedAtMin == null is now an additional gate on the charging banner render. "Waiting on battery" is nonsense on a finished quote regardless of the data state.
  • One-shot boot backfill backfillOrphanChargingOnCompletedQuotes() walks state.quotes once at boot and idempotently closes any orphan charging windows on completed work quotes (pushes a {start, end} into chargingHistory, clears charging + chargingStartedAt). Past-day archives are left alone — frozen records of how the day was. Restores actuals-math fidelity for any pre-v1.8.203 charging events that hung around.
  • Manual time-edit paths now close charging too — the inline finish-timestamp pill and showEditQuoteTimesModal both detect a transition into "complete" and call closeAnyOpenChargingOnQuote(q), matching the FINISH-button behaviour.
v1.8.210

Pitch deck — honest Google Sheets framing (team's on paper today)

The deck claimed "bi-directional Sheets sync against the team tracker" which would jar any manager who knows the team currently runs on paper sheets, not a Google Sheet. Reframed across slides 1, 2, 5, 7, 8, and 9 to be accurate: team's tracker is paper today; the Google Sheets integration is ready for the day it goes digital.

  • Slide 1 lead — dropped "bi-directional Sheets sync against the team tracker"; now reads "a Google Sheets sync layer ready for the day the team's paper tracker moves digital".
  • Slide 2 lead — "The team tracker is a spreadsheet" → "The team's tracker is paper. Totals get aggregated at EOD, drift only becomes visible in hindsight, and mid-day changes don't surface anywhere until tomorrow's reconciliation."
  • Slide 5 — eyebrow now "Tracker reconciliation · digital-ready" (was "bi-directional"); lead positions the integration as built against the paper-tracker structure and wired against a Sheet that mirrors it today, slotting into a team-wide Sheet without code changes when paper migrates.
  • Slide 7 Wex column — "Adjustments flow back to the master sheet automatically" reframed as "A digital twin of every day — the kind of granularity paper sheets can't easily preserve."
  • Slide 8 Google Sheets row — tag changed from "bi-directional" to "digital-ready"; description acknowledges current usage against a mirror Sheet.
  • Slide 9 Google Sheets safety row — "same sheet access you already have" softened to "the app only sees Sheets that account has already been given access to", which lands cleanly even for a reader who hasn't used Google Sheets themselves.
v1.8.209

Pitch deck — slide 3 mock now mirrors the real app's quote-card structure; sharp pulse-dot

Slide 3's workflow mock is now accurate to the app, not a stylised stand-in. The generic .row + mat-icon placeholder structure (which kept trying to render check_circle / radio_button_checked as text-ligature icons) is gone. Replaced with .mock-quote-row + .mock-qcheck markup whose CSS tracks app.css 1:1: QIDs now render in the same sharp surface-2 + line-border boxes the real .qid uses (green border when done); status pills follow the same ACTIVE / DONE / SCHEDULED format and colour as .qstatus-pill; item checkboxes are 13px boxes coloured by phase — inspect = --ok (green), nova = --accent (lime), divider, admin = --info (blue) — with a CSS-rendered checkmark via rotated borders, same trick app.css uses on the real 22px .qcheck. Same UI, just smaller.

Sharp pulse-dot. The active-quote pulse-dot and the mock-live indicator both used border-radius: 50%, which violates the project's no-rounded-corners design rule AND was rendering visibly stretched/elliptical in some viewports (likely due to flex-item cross-axis behaviour in the small mock context). Both are now sharp 8×8 / 6×6 squares with explicit flex-shrink: 0 and vertical-align: middle.

v1.8.208

Pitch deck — Material Symbols rule aligned with the app, fixing remaining literal-text icons

v1.8.207's text-transform: none / letter-spacing: normal patch wasn't enough — icons in slide 3's workflow mock were still rendering as literal "check_circle" / "schedule" / "radio_button_checked" text. Pulled the full .mat-icon rule from app.css (where icons render fine everywhere) into pitch.html's inline style block. The missing pieces that mattered: white-space: nowrap (so the underscore in "check_circle" doesn't get treated as a soft-wrap point that splits the ligature mid-lookup), display: inline-block, and font-variation-settings for the variable font's FILL / wght / GRAD / opsz axes.

v1.8.207

Pitch deck — broken icons in the workflow mock (uppercase ligature break)

The new state icons in slide 3's enriched workflow mock (check_circle on done rows, schedule on queued rows) were rendering as literal text — "CHECK_CIRCLE" instead of the glyph. Same root cause as the slide-12 CTA icon fix in v1.8.206: an ancestor with text-transform: uppercase (here .mock .row span:first-child) was inherited by the nested .mat-icon span, and Material Symbols ligature lookup is case-sensitive.

Fix is now global — added text-transform: none; letter-spacing: normal; to the .mat-icon base rule itself, so icons resist any future uppercase ancestor without per-site overrides. The earlier .cta-link .mat-icon override is now redundant but kept as belt-and-suspenders.

v1.8.206

Pitch deck v3 — Safety slide, Improvement-not-surveillance slide, workflow mock, counter fix

Two new slides + a fundamentally fixed counter animation. The 12-slide deck now opens up the framing past "features and integrations" into the harder questions a department head will actually have.

New Slide 7 · Improvement, not surveillance. Three-column benefits split by audience — what changes for the inspector, the manager, and Wex as a whole. Closes with a mono "what the app refuses to do" line (score · rank · shame · expose data without explicit access · turn the workday into a game to win). Sits right after the manager-view slide so the reader's natural reaction ("am I supposed to police my team with this?") gets answered before it solidifies.

New Slide 9 · Safety & IT compatibility. Mirrors slide 8's integration-row visual but each row is a safety statement: NOVA deep-link only (no credentials, no API), Sheets-as-your-own-account (no service account, no shared password), Teams no admin needed (no Azure AD reg, no Graph permissions), hosting outside the Wex perimeter, your data per-user isolated by Firestore rules. Directly addresses the "is this safe to put in front of inspectors?" question.

Enriched Slide 3 workflow mock. The "Today" panel now shows a horizontal day-flow strip at the top (08:00 → 10:00 → NOW → LUNCH → 13:00 → 16:30, with the NOW segment glowing), Material-Symbol state icons inline on each row (check_circle for done, schedule for queued), and a nested item-detail block under the active quote showing the three-step inspect/nova/admin checkboxes per item. Communicates the actual workflow visually instead of just listing QIDs.

Counter animation fixed. The v1.8.205 counters were stuck at 0 — Motion One 10.x's animate() doesn't fire onUpdate when you pass a plain object as the first arg the way Framer Motion does. Replaced the implementation with raw requestAnimationFrame + ease-out-cubic interpolation. Slide 2's "Inspectors run 20+ quotes a day", slide 9 (now 11)'s "200+ shipped releases", and every other stat tile / counter / hour-minute display now actually rolls up from 0.

CTA icon fixed. The slide-12 "Open the live app" button was rendering the literal text "NORTH_EAST" because .cta-link's text-transform: uppercase was inherited by the nested .mat-icon span and broke the Material Symbols ligature lookup. Added an explicit text-transform: none override.

v1.8.205

Pitch deck v2 — full integrations slide + heavy animation

Integrations slide rewritten as the ecosystem narrative. The pitch now has a dedicated slide listing all five integration surfaces in a hub-and-spoke layout: Google Sheets (bi-directional read+write), Product Lookup catalogue (6,400+ SKUs), Microsoft Teams (deep-link DM), Firebase (auth + sync), NOVA (deep-links). Each row has its name, the arrow type (sync / read / write / deep-link), and a one-liner on the mechanism. Slide 5 (tracker reconciliation) now also stresses the write-back capability.

Heavy animation pass. Loads Motion One via CDN. Per-slide entrance via inView: every .anim-pending child fades up with a 30ms stagger, individual elements can opt into a longer delay via data-delay. Counters across the deck (20+, 33, 7h, percentages, item counts) roll up from 0 using cubic-bezier ease-out. Cover brand-mark SVG bars scale-in from scaleX(0) with elastic overshoot. Slide 3 has a live ticking mock — the "in progress" row updates every 1.8s; the day total counter rolls up to 6h 44m. Slide 8 history mock includes a 7-day heat strip that scales up cell-by-cell and an SVG sparkline that draws in via stroke-dashoffset. Final CTA button has a continuous pulse-glow. Cards lift on hover (translateY + accent border + soft shadow). Cursor-following radial gradient on body so the whole deck has a subtle light source that tracks the mouse. Floating background shapes parallax-shift on scroll. CSS keyframes drive the active-row pulse-dot, mock-live blink, and CTA shine.

Graceful degrade. If Motion One fails to load (CSP, offline), .anim-pending elements get their opacity restored immediately so the deck still reads — no animation, but no broken content either.

v1.8.204

Pitch deck — interactive slide show at /pitch.html

New standalone page at inspection.dannylaw.dev/pitch.html — a 10-slide deck Danny can send to his dept head. Styled in the app's mono / sharp / accent vocabulary. Scroll-snap full-height slides, keyboard nav (↑↓ ←→ space PgUp PgDn Home End), click-to-jump dot indicators at the bottom, prev/next arrows on the right edge.

Slides: cover · the problem · paste-the-day solution · live pacing (active vs elapsed) · tracker reconciliation · manager dashboard · Teams integration · history & insights · ADHD-aware design + free to run · call-to-action with link to live app. Self-contained: no auth, no Firebase needed, just open the URL.

v1.8.203

Auto-close battery charging when a quote finishes or archives

Items left in charging: true when their quote finished had no UI path to stop the charge — the item context menu only renders against in-progress quotes. Now: any open charging window is closed automatically at three points.

  • userFinishQuote — tapping FINISH closes charging on all items in that quote. New helper closeAnyOpenChargingOnQuote(q) stamps an { start, end } entry into each item's chargingHistory and clears the live flags. Toast announces "Stopped charging on N items".
  • maybeStampCompletion — the all-missing edge case (quote auto-finishes because every item is missing) closes charging the same way.
  • archiveCurrentDay — closes charging on every work quote being archived, not just the ones auto-finished. Archived snapshots no longer preserve orphan charging state into history.
v1.8.202

Lunch auto-pause + auto-resume

Active quotes used to tick through the 30-minute lunch break — live elapsed counted it, and actuals got inflated up to the 20min gap-cap. Now: at LUNCH_START, the currently-active quote auto-pauses with a toast ("Lunch · paused QID"). At LUNCH_END, if the SAME quote is still paused and unfinished, it auto-resumes with a toast ("Back from lunch · resumed QID"). Per-day tracking in state._lunchAuto ensures each side fires at most once per day. Manually-paused quotes are untouched; quotes you finished, removed, or replaced during lunch don't get auto-resumed. Driven from tickClock so latency is ≤30s on either side.

v1.8.201

Drop redundant top rail toggle button

The top rail button (with the menu_book icon) only toggled the sidebar open/closed without picking a tab. But every tab icon already does close-on-active-click (introduced in v1.8.178's pickTeamsRecipientThen tab-switch logic), and close-without-tab-change is covered by the in-sidebar × button and the Escape key. Removed the toggle from the rail. Rail now has 4 tab icons (Input · Lookup · Phrases · Day) plus Auto-sort and Settings — six icons total, all with distinct purposes.

v1.8.200

Lookup row sizing fix for sidebar + icon-only NOVA / Add buttons

Real cause of the "names not showing" report: .lookup-row uses a 3-column grid 1fr 200px auto designed for the full-width Lookup tab (max-width 900px). Inside the sidebar's ~480px pane the fixed 200px middle column squeezed the name column to near zero. The 720px media query catches narrow VIEWPORTS but not narrow CONTAINERS, so it never fired on desktop. Added .sidebar-pane-lookup .lookup-* overrides: 2-column grid, meta cell wraps below on its own row, smaller fonts throughout, and the wrap's 24px padding/900px max-width neutralised since the sidebar pane provides its own.

Plus: NOVA and Add-to-quote action buttons now icon-only with title tooltips. Saves horizontal space in the cramped sidebar context. NOVA = open_in_new, Add = add, disabled-no-SKU = block.

Z-index fix: .modal-backdrop was at z-index: 100; sidebar is at 110 and the rail at 120. Opening a confirm modal (or Settings) from inside the open sidebar drawer made the modal appear BEHIND the drawer. Bumped to z-index: 200 so all modals sit above the sidebar layer.

v1.8.199

Lookup pane shows catalogue results, sync button restyle, delta toast

Three issues from the v1.8.198 testing.

  • Names missing from the Lookup pane. Not actually a name-data bug — the Lookup search was only querying itemLibrary (items you've inspected before). The 6,414 products you synced into _productLookup were invisible to the search. Now _runLookupSearch queries BOTH and renders them in two sections ("N from your library" / "M from Wex catalogue"). Catalogue rows are deduped against library rows by lowercased name + SKU. Empty state shows recents + a "Catalogue · 6,414 products — type to search" hint.
  • Re-sync button styling. Was btn-sub which doesn't render with the proper button frame. Changed to btn with a tightened font-size + padding so it sits inline cleanly with the status row.
  • Sync delta toast. The sync click handler now captures the SKU set before sync, then compares after, so the toast reports the actual change: Synced 6,414 · +12 new · 6,402 unchanged. First sync shows "first sync"; zero changes shows "no changes"; otherwise reports added / removed / unchanged.

New catalogue rows render with a small "catalogue" chip in place of the usage count, and the row name in --ink-soft so library rows visually take precedence (they have usage data; the catalogue is just the lookup surface).

v1.8.198

Product Lookup sync bar on the sidebar Lookup pane

Promoted the catalogue sync out of Settings → Integrations and onto the sidebar Lookup pane. Top of the pane now has a status bar with: a status dot (green/amber for ok/stale), a one-line summary ("6,414 products · synced 12m ago"), and a Re-sync button. Three states render: no sheet ID configured → hint pointing to Settings; not yet synced → "Sync now" CTA; synced → count + relative time + "Re-sync". Re-paints on every renderLookupView call so timestamps stay current. Same syncProductLookup() handler the Settings panel uses; no behavioural duplication.

v1.8.197

Settings modal overhaul — internal tabs + 4 new settings

The Settings modal had ~9 sections to scroll. Restructured into 5 internal tabs: Day (workday + daily template) · Behaviour (automation, sound, notifications, routines, display, theme) · Sharing (viewers + preparers) · Integrations (Sheets + audit interval + Teams contacts) · Data (storage, library, clear-data, sign-out). The old v1.8.149 jump-nav is gone; each panel is one screen. 120ms fade-in when switching tabs.

Four new settings:

  • Density — Comfortable (default) or Compact. Compact tightens line-heights and paddings throughout via body.density-compact class.
  • Sound granularity — single soundEnabled toggle replaced with separate Completion tone and Active-cross chime toggles. Backwards-compat: pre-v1.8.197 users with sound on get both new flags treated as on; legacy soundEnabled stays in sync.
  • Audit auto-refresh interval — 5 / 10 / 30 / 60 minutes. Was hardcoded to 10. Useful during active reconciliation vs background-only.
  • Clear data buttons — 4 targeted clears (actuals / item library / audit cache / tracker reversals). Each confirms before clearing. None affect today's live state.
v1.8.196

Notifications — archive toast cascade + paused-quote near-end push, with permission flow

Two additions under the relaxed rule. Both off by default.

  • Archive toast cascade at +60/+90 min past workday end. The main banner already fires at +30m; these are lighter toasts that nudge twice more if the day is still unarchived. One per threshold per day, stored in localStorage. Suppressed if the main nudge is snoozed or dismissed for the day, or if post-archive mode is active.
  • Browser notification when a paused quote nears its planned end. Fires once per quote when a PAUSED quote's projected end (start + planned + accumulated pause) is within 5 minutes. Settings → Behaviour → "Browser notifications" — toggle requests OS-level permission inside the click handler (Notification API requires a user gesture). If denied or unsupported, the toggle reverts and shows a toast explaining why.

Both checks live in tickClock (30s cadence) so the latency on either is within half a minute. New helpers: fireBrowserNotification, requestNotificationsPermission, maybeFireNearEndNotifications, maybeShowArchiveToastCascade.

v1.8.195

Sound cues — completion tone + active-cross chime, off by default

Two short sine tones, gated on a new Settings → Behaviour → "Sound cues" toggle (off by default).

  • Completion tone — 880Hz sine for 60ms with a 20ms fade. Plays on userFinishQuote. Reads as a "tick", not a fanfare.
  • Active-cross chime — 660Hz, same envelope. Fires once per quote when its live elapsed time first reaches/exceeds its planned time. Guarded by q._crossPlannedFired so it doesn't replay on every 30s tick.

WebAudio context lazy-created on first user gesture (browsers reject autoplay otherwise). If context creation fails, playTone silently no-ops. No music, no chord, no chord — just a clean state-change confirmation.

v1.8.194

Factual EOD heroline

The End-of-Day modal's heroline used to randomly pick from sets like "Day done / Wrapped / See you tomorrow / Logged" or "Clean sweep / Everything done / Full day, all wrapped". Replaced with a deterministic factual format that reports the day's actual numbers every time: Clean sweep · 7h 9m worked · Target met · 7h 0m worked · All quotes wrapped · 6h 30m worked · Short by 30m · 6h 30m of 7h 0m · 5h 12m worked · 1h 48m short of target · 0m worked today · Quiet day when no work was scheduled. Same lifecycle cases the random version covered; the line now means what it says.

v1.8.193

History surfaces — last-7-days line, milestone markers, pace sparkline

Three additive history/EOD surfaces under the relaxed rule. All factual — no celebration copy, no scorekeeping mechanics.

  • Last-7-days line above the heatmap. "Last 7 archived days: 5 on target · 1 partial · 1 under 70%" — uses the same classification thresholds (hit / partial / short) the heatmap cells use. Reads as a one-line summary above the 12-week grid.
  • Milestone markers in the history list. Thin chronology rows inserted above the day that crossed each milestone (100, 500, 1000, 2500, 5000, 10000 completed quotes archived). No popup, no animation, no congratulation. Renders as "Milestone · 500 quotes archived" with accent border, same vocabulary as the week dividers.
  • Pace card sparkline in the EOD modal. 60×16 inline SVG showing the last 7 archived days' on-target % as a single accent-coloured line. Below the existing pace value. Pure data, no axes or labels.

New helper buildInlineSparkline(values, opts) available for future reuse — minimal SVG polyline, accepts an array of numbers plus optional max/min/width/height/stroke.

v1.8.192

Polish cluster — periods, rail tap, Day-tab anchor, fade, Auto-sort on rail

Five small polish items, mostly from the Chrome Claude review.

  • #13 EOD modal heroline periods. Trailing full stops dropped from the End-of-Day hero phrases ("See you tomorrow", "Target hit", "Clean sweep", "Wrapped", etc.). Brings them in line with the mono technical typography of the surrounding labels.
  • #16 Rail tap targets. Rail buttons resized from 34×34 inside a 44px-wide rail (with 5px dead strips either side) to 44×38 — the button is now the full rail width, so the cursor changes and click works across the whole rail row.
  • #18 Day-tab Archive button anchored. Archive Day now sits at the bottom of the Day pane via margin-top: auto on a new .sidebar-pane-day-foot wrapper. Keeps the destructive action separated from the safe utility actions and lands it at thumb-distance.
  • #23 Sidebar tab cross-fade. Switching internal sidebar tabs now plays a 120ms opacity keyframe fade-in. Keyframe rather than transition so it works through display:none → flex (transition wouldn't fire).
  • New: Auto-sort on the rail. Moved from the Day pane to a dedicated rail button right above the settings cog. One-tap quick action, no need to open the sidebar.
v1.8.191

Lookup empty rows hidden, "restoring unsynced" toast no longer fires on every reload

Two Chrome Claude review items.

#21 Lookup recently-used empty rows. The Lookup pane's "Recently used" list was rendering rows with no product name AND no SKU — showing only "5M USED 1×" / "8M USED 1×". These were library entries with whitespace-only names from old/partial imports. They're not actionable (can't search, can't open in NOVA, can't usefully add) so the renderer now skips them entirely. Recents list pre-filters too so they don't burn slots in the 12-item cap. Rows that have a SKU but no name now show "(no name · 3051102)" as a clear fallback rather than rendering blank.

N5 Restoring-unsynced toast on every load. The "Restoring unsynced local changes" toast was firing every reload even when no real user edits were pending. Root cause: markLocalEdit() was called from inside the wrapped saveState / saveHistory / saveSettings / saveActuals / saveItemLibrary functions unconditionally — including during boot-time backfills (active-time recompute, daily-target backfill, etc.). That bumped lastLocalEditMs above lastFlushedMs from the prior session, so localIsFresherThanCloud() always returned true and the toast always fired. Fix: gate markLocalEdit() on _syncReady && !_syncSeeding (same gate the queue-for-upload paths already used). Boot-time auto-mutations still update localStorage but no longer poison the freshness check; the toast now only fires when there's a genuine unflushed user edit to preserve.

v1.8.190

Stop Daily Allocation parse at first blank row after today's chunk

Sometimes the preparer pastes tomorrow's allocation BELOW today's in the same Daily Allocation tab, separated by blank rows. With v1.8.189's widened fetch range, those rows were being read in and tomorrow's qids leaked into today's audit (every one would appear as a "tracker-only" verdict). Fix: track an inChunk flag; once the parser sees its first real row it's "in" today's chunk; the very next truly-blank row terminates parsing. Leading blanks (before any data) still get skipped. Partially-populated rows (e.g. a stray "TOTAL:" cell with no item name) are noise, not blank — they continue past but don't end the chunk. Works regardless of how many blank rows the preparer leaves between chunks; one is enough.

v1.8.189

Widen Daily Allocation fetch range — A9 → A2

The audit's tracker fetch was hardcoded to start at row 9 of the Daily Allocation tab ('Daily Allocation'!A9:C). That was an off-by-N guess about where Wex's actual data starts. If the tracker's data begins higher up (row 7-8), those rows get silently missed — and the audit flags the affected qids as "app-only" even though they're clearly in the sheet. The row count from the Allocation tab still agrees because that comes from a COUNTIF formula on the whole column, masking the parse miss. Widened the range to start at row 2; the parser's qid regex already filters out header rows, so widening can only catch more real data, never add false positives.

v1.8.188

Audit qid tolerance — strip non-digit chars before validating

Three quotes in Danny's tracker (71581964, 71586212, 71583752) were being silently flagged as "app-only" by the Audit despite being clearly present in the Daily Allocation tab. Root cause: the tracker fetch's qid validator was /^\d{4,15}$/ — strict digits-only. If Sheets returned a cell with thousand separators ("71,581,964") or a hidden character (NBSP / U+202F narrow-no-break-space, both of which Sheets sometimes auto-inserts when number cells are copied from other sources), the row failed the regex and got silently dropped from the tracker map. The Day target check still showed matching totals because it reads from the Allocation tab's COUNT formula, masking the parse failure. Now: fetchDailyAllocationFromSheets strips every non-digit character from the qid before testing length. Cell-formatting weirdness no longer hides rows from the audit.

v1.8.187

Progress strip moves from sidebar back to header

The "0 / 0 items done · 0m scheduled left · Plan 0%" strip used to live at the top of the sidebar, pinned above the internal tabs. Moved it back into the always-visible header area — sits as a thin row between the header and the tab bar, still inside .sticky-block so it stays visible during scroll. Single-line compact layout: items done · scheduled left · Plan bar with width-flexible track · optional "N ready to finish" nudge anchored to the right. All element IDs preserved (statItemsDone, statTimeLeft, progressPct, progressFill, dayTargetPct, dayTargetFill, readyToFinishRow, readyToFinishCount, readyToFinishBtn) so existing render code finds them unchanged. Sidebar's pinned-Progress block is gone; the sidebar now starts directly with the internal tab bar.

v1.8.186

Sidebar phrases auto-group by label prefix

Reference phrases in the sidebar's Phrases pane now auto-group by the part before the first · in their label. The seed phrases already follow this Group · Variant convention ("Odour · musty", "Mite · Inspection Notes", "Battery · Canon (full)"), so the grouping appears naturally without any schema change. Each group renders as a collapsible block with a header showing the group name + item count; inside the group, each row shows just the variant text (the prefix is implied by the group). Phrases without a separator fall into an "Other" group at the bottom. Expansion state persists per-group via localStorage (key dayalloc_phraseGroupExpanded_v1) so closed groups stay closed across reloads. The add-form's label placeholder updated to Odour · musty as a hint.

v1.8.185

Follow-up: N1 contradictory-state blocker + 6 quick fixes

Seven items from the v1.8.184 audit. One blocker, the rest copy / contrast / a11y polish.

  • N1 BLOCKER — contradictory page state after pull-from-history. If you pulled a quote back into today while post-archive mode was active, three sources of truth showed at once: the "Day archived" recap hero, the "Workday ended — archive today?" banner, and a running LIVE quote pill. Two fixes: (1) pullArchivedQuoteIntoToday now calls markUserActivityIfPostArchive() so pulling implicitly exits post-archive (the act is "I'm working again"); (2) maybeShowAutoArchiveNudge early-returns when post-archive mode is active, so the banner can't fight with the recap hero either way.
  • N2 — duplicate ACTIVE pill. Row 1 had a legacy .active-pill appended by updateActiveState; row 2 had the new .qstatus-pill.qstatus-active from v1.8.177. Removed the legacy append (the row-2 chip is the source of truth now). Stale pills from old renders are cleared on next update so deploy doesn't leave a transient duplicate.
  • N3 — header band over-signalled red/amber. .day-status.sev-warn / .sev-danger were tinting the whole 5-cell frame red/amber even when only 1-2 cells were genuinely out-of-spec. Removed the frame tinting; per-value colour (already present on .ds-value) is enough.
  • N4 — low contrast on row-2 time-pill labels. .tp-label was using --ink-faint (~3:1 on dark) for core facts like "STARTED 18:55". Bumped to --ink-soft for legible secondary text.
  • N6 — LIVE pill was doing four jobs. Dropped the redundant "→ HH:MM" projected-end inside the live-pace pill; row 1's .qtimewindow already shows the schedule arrow with that exact value. LIVE pill now: label · elapsed / planned · remaining.
  • N7 — aria-controls null on sidebar tabs. Added aria-controls on each .sidebar-tab pointing at the new #sidebarPane{Input,Lookup,Phrases,Day} ids. Renamed sidebarPhrasesHost to sidebarPanePhrases for naming consistency (renderSidebarPhrases updated to match).
  • #12 — "pick up work" imperative softened. Workload-balance card now says "under target" / "over target" instead of "pick up work" / "can give back". Factual labels work in both live workday and EOD recap contexts; the directive tone read as a soft scold in the closing flow.
v1.8.184

Content / clarity cluster from Chrome Claude review

Seven items from the v1.8.183 audit, all label / icon / copy fixes — no new behaviour.

  • #8 Spillover pill rewording. "X over" was ambiguous (over what — plan? lunch? workday?). Re-labelled to "EOD +Xm" so the scope is obvious. Lunch-crossing copy unchanged.
  • #9 Active-time pill always visible. Used to hide entirely on a just-started quote (active=0, elapsed=0); when it did render mid-flight at edges it collapsed to a label-less pill. Now always shows "active Xm · elapsed Ym" once started — even at 0/0 — and only goes .diverged styling when elapsed exceeds active by >20%.
  • #10 "X / Y" progress chip gets an "items" label. Was floating bare next to other pills; now reads "3 / 7 items" with matching aria-label.
  • #11 Pause / Resume button gets an icon. NOTE button had its edit_note glyph; PAUSE didn't have anything. Now pause when running, play_arrow when paused — matches the existing button-icon vocabulary.
  • #14 Sidebar head label is dynamic. Was "Quick access" (vague); now mirrors the active internal tab — "Input" / "Lookup" / "Phrases" / "Day actions". Updates on every tab switch.
  • #15 Phrases rail icon swapped. Was content_copy (reads as "copy"); now text_snippet (reads as "library of reusable snippets"). Added to the Material Symbols allowlist.
  • #19 "+ Add to schedule" gets a proper icon. Was using a literal + text glyph among a row of Material-icon buttons; now uses add alongside the others.

Bonus a11y: sidebar tab buttons now set aria-selected and panes get aria-hidden/role="tabpanel" — Chrome Claude flagged the missing screen-reader wiring.

v1.8.183

Hot-fix: renderStats null crash, sidebar bottom gap (proper fix), darker scrim

Chrome Claude review caught three issues — two blockers + one annoying — all addressed here.

Blocker 1 — renderStats null crash. document.getElementById('progressTag').textContent = … at app.js:6244 threw a TypeError because the #progressTag element only existed in the v1.8.178-removed right-aside Progress panel. The throw aborted the rest of renderStats, which silently broke five downstream call paths: archiveCurrentDay, startQuoteNow, pullArchivedQuoteIntoToday, initSyncLayer, and bootApp. Every render of those flows was failing without surfacing the error. Replaced the direct DOM writes with setText/setHtml/setStyle helpers that no-op when the element is absent.

Blocker 2 — sidebar 160px bottom gap (root cause now identified). v1.8.182 added top: 0 + bottom: 0 + height: 100dvh belt-and-braces, but the gap persisted because the base aside { } element rule (left over from the OLD sticky right-aside layout) was setting max-height: calc(100vh - 160px) and overflow-y: auto. Element-selector specificity is lower than class, but .sidebar never explicitly overrode those two properties — so they kept winning. Spelt out max-height: none and overflow-y: visible on .sidebar. The drawer now reaches the bottom of the viewport.

Annoying 3 — scrim too transparent. v1.8.181 used rgba(0,0,0,0.18) which barely registered visually; Chrome Claude flagged that the drawer floats over a still-bright timeline that keeps competing for attention. Bumped to rgba(0,0,0,0.45) (Material's default).

Nitpick 4 — scrim stayed display: block when closed. The scrim element was always rendered, just transparent + pointer-events: none. Now visibility: hidden when closed, with the transition delayed so the fade-out still plays before hiding.

v1.8.182

Sidebar polish — bottom-gap fix, bigger rail icons

Two small but visible v1.8.181 follow-ups. Bottom gap on the sidebar drawer: devtools showed the element extending to the viewport bottom, but visually a gap persisted — root cause was the flex column not claiming the leftover vertical space when only height: 100dvh was set. Added top: 0 + bottom: 0 alongside the height so the box is anchored to both viewport edges; same defensive layering applied to the rail. Rail icons too small: 16px glyphs in 28×28 buttons inside a 36px rail were hard to hit and read. Bumped rail width 36 → 44px, button size 28 → 34, icon size 16 → 20. .app padding-left bumped 56 → 64px to match. Mobile breakpoint's calc(100vw - 36px) updated to - 44px to keep the full-width overlay clean.

v1.8.181

Version chip + sync indicator move back to the header

v1.8.178 had relocated both into the sidebar's top bar. The cost was that they only became visible on sidebar-open, which is the wrong default for two of the most "always-on" status surfaces in the app. Reverting that move: version chip is back inside the <h1> as a link to release notes, sync indicator is back in the header's clock cluster. Settings cog stays in the sidebar. Sidebar head now reads "Quick access" with just the settings + close affordances.

v1.8.180

Post-archive day state + sidebar height & animation polish

Post-archive state. After Archive day, the main page used to look identical to a live workday — same quote cards, same workload-journey strip — just with the next day's auto-applied template. Now it enters a visually distinct post-archive mode: the workload-hero is replaced with a "Day archived" recap card (yesterday's hours vs target, quotes done, missing count, added/dropped vs original allocation), and the quote cards rendered for tomorrow's template are visibly muted (55% opacity, accent-line dropped). Hover or focus on a card temporarily restores full opacity so you can inspect without dismissing. The mode exits automatically at 07:00 the next calendar day (caught by the 30s tick), or instantly when you tick any item (via maybeStampStart's user-activity hook), or manually via a "Start day now" button on the recap. State persists across reloads via state._postArchiveUntil.

Sidebar fixes. Height now uses explicit 100vh / 100dvh instead of top:0 + bottom:0 — was producing a visible gap on some viewport configurations and didn't follow mobile address-bar resize. The slide animation was overshooting: translateX(calc(-100% - 36px)) added 36px of extra travel that stalled the open. Simplified to translateX(-100%) with a material-style cubic-bezier(0.4,0,0.2,1) curve at 240ms, plus will-change: transform for smoother GPU compositing. Rail z-index bumped from 100 to 120 so the rail icons stay clickable while the sidebar is open — lets you switch between tabs without closing first.

v1.8.179

Hot-fix: null-guard the removed Input panel wiring (v1.8.178 boot regression)

v1.8.178 removed the right-aside Input panel HTML but left two top-level document.getElementById('inputPanelHead').addEventListener(...) and ...('pasteBox').addEventListener('blur',...) calls in place. The inputPanelHead element no longer exists, so boot threw TypeError: can't access property "addEventListener", document.getElementById(...) is null at app.js:9919 and stopped initialising. Wrapped both wirings in null-guards so they no-op when the elements are missing (which is now always). No behaviour change — the sidebar's Input tab is the new home for that workflow.

v1.8.178

Layout restructure: collapsible left sidebar replaces the right panels, Lookup tab dropped

Big shell-level change. The right <aside> (Input / Progress / Actions panels) is gone — everything moved into a new collapsible left sidebar. A thin always-visible rail (36px wide) sits on the left edge with icons for Input / Lookup / Phrases / Day actions plus a settings cog at the bottom. Click any rail icon → sidebar slides over the page as an overlay (1/3 viewport on desktop, full-width on mobile below 720px). Click the same icon again, hit Escape, or click outside → closes. Main content stays full-width whether open or closed (overlay model, not push-content).

Sidebar contents:

  • Top bar: version chip (links to release notes) · sync indicator · settings cog · close × button. Moved off the header.
  • Pinned Progress strip: X/Y items · Nm scheduled left · Plan bar · "ready to finish" nudge. Always visible regardless of which internal tab is active.
  • Input tab: paste TSV box · Parse Day / Merge · Add to schedule · Queue tomorrow.
  • Lookup tab: Product Lookup search + result list. The old top-level Lookup tab was removed.
  • Phrases tab: quick-copy reference phrases — promoted out of the Reference modal into its own sidebar pane so they're one click away during inspection. Add / delete / tap-to-copy.
  • Day tab: Auto-sort · Export · Shutter / grading table (launches the Reference modal, which now contains only the table) · Archive day.

The header keeps brand mark, day-status strip, viewer-presence, keyboard-help, viewer-preview, clock. Version chip / sync indicator / settings cog all moved to the sidebar top bar. The top tab bar lost the Lookup entry; everything else (Today / History / Stats / By category / Audit / Status) is unchanged.

v1.8.177

Active inspection time, battery-charging state, quote-head redesign — fixes the 400% recorded runs

The actuals were polluted: a quote you started, then stepped away from (overwhelmed, helping someone, charging a battery without tapping Pause), then came back to, was recording the full wall-clock elapsed as "actual time spent". One quote was showing 400% over its plan. Big rework:

Active inspection time — new metric. quoteActiveInspectionMin(q) walks the item tick timestamps (inspectedAt / novaAt / adminAt, v1.8.94+), sums the durations between consecutive ticks, and caps any individual gap at 20 minutes. The remainder is "stepped away" and excluded. The actuals recording at archive time now uses this instead of (completedAtMin - startedAtMin - totalPausedMin). The wall-clock formula still drives the in-progress "elapsed" display but no longer pollutes long-term pace data. One-shot boot backfill re-derives every past actuals.byQid[*].runs[*] entry whose archived quote still has tick timestamps — your 400% runs get rewritten with the active value, idempotent via _recomputedAt. Pre-v1.8.94 history is untouched (no ticks to work from).

Battery-charging state — item context menu gets "Mark charging" / "Done charging". When set, the item's chargingHistory records the start/end window, and any inter-tick gap that overlaps the window is excluded from active-inspection-time regardless of length. So a 4-hour battery charge no longer counts as 4 hours of "active" work. Multiple charge cycles per item supported. A small "charging" chip appears on the item row while active.

Quote-head redesign — two-row stacked layout. Row 1 = identity (drag handle, qid, Open/Spares links, planned minutes, scheduled time window, notes, expand). Row 2 = state (timestamps · status pill · 3/7 progress · active-time pill · remaining · perf pill · action buttons). The old single-row "qduration with seven pills inside" structure was crowded; splitting gives each cluster room to breathe.

New pills: merged status pill (Active/Paused/Next, was three separate things). Active-time pill (shows "active 28m"; expands to "active 28m / elapsed 45m" when elapsed exceeds active by >20% — meaningful step-away signal). Remaining pill (extracted from the old plain-text "12m left"). Charging banner above the head when any item is charging: "2 items waiting on battery · 1h 20m so far".

Dropped: almost-done pill ("1 left to admin") — the 3/7 count and item list already say what's left. Resumed-but-paused-history info pill — the active-vs-elapsed divergence now surfaces this naturally. Separate Active + Next-up pills — merged into the status pill.

v1.8.176

Restore viewer-hero CSS — regression from v1.8.76 (Compare/Elo tab removal)

Hot-fix. Back in v1.8.76 (May 13), removing the Compare/Elo tab also accidentally deleted the .viewer-hero / .vh-* CSS block from app.css. renderViewerStatsView and the EOD modal recap section both reference those classes — so the manager Status view and the End-of-Day card have been rendering as bare unstyled text for ~2 weeks, with everything smushed together with no card layout or grid. Restored the block from the pre-v1.8.76 commit and added .vh-status.danger / .vh-status.ok variants which the current code emits but the original CSS didn't define.

v1.8.175

Teams quick-message: EOD recap button + per-row "Notify manager" on audit mismatches, editable preview

Two new Teams entry points layered on top of v1.8.174's foundation. (1) EOD recap — the End-of-Day modal foot gains a "Send recap via Teams" button between Cancel and Archive. Clicking opens an editable preview modal pre-filled with the same numbers the EOD card shows: hours vs target, quotes done, missing items list (capped at 12 with overflow note), and original allocation → current allocation breakdown with pickup/drop deltas. You can tweak the text before sending. The recipient picker (from v1.8.174) then opens the DM via deep-link. EOD modal stays open underneath so you can still archive after sending. (2) Audit row "Notify" — each per-quote audit row that isn't "match" (so mismatch / app-only / tracker-only) gets a small "Notify" button at the right edge of the row head. Click → preview modal pre-filled with a row-specific message: for mismatches it lists the per-item diffs (app-only / tracker-only / minutes-diff) with values, capped at 10. Same picker + send flow. Edit before sending is supported on both. The preview modal stacks above any other open modal so the EOD or Audit context isn't lost.

v1.8.174

Teams quick-message: deep-link DM button on missing items, Settings → Teams contacts panel

First slice of the Teams integration (no webhooks, no API, no IT permission needed). The "send a Teams message about this missing item" capability used to be a clipboard-copy button that put a pre-formatted note in your clipboard for manual paste. v1.8.174 adds a sibling button right next to it — Teams icon — that opens a Teams DM with the same pre-filled message via the public teams.microsoft.com/l/chat/ deep-link. You hit send. Recipient list is built from three sources at picker-open time: viewers (from Sharing → Viewers), preparer (from Sharing → Preparer), plus a manual contacts list in Settings → Teams · quick-message contacts for anyone you don't want to grant read-access to. Zero contacts configured → button hides. One contact → opens directly. Two or more → a small inline picker appears beside the click. Channels can't be deep-link targets (Teams doesn't accept a ?message= param for channel URLs), so channel posts continue to use the clipboard-copy button which is still there alongside.

v1.8.173

Time / wall-clock: legacy history target backfill, untracked-block archive marker, classifyDrift boundary doc — closes the 21-finding audit

Final three fixes from the logic audit. (1) History rows that pre-date the dailyTargetMin snapshot feature were silently re-grading themselves whenever the global target setting changed — a 7h day showed 95% one week and 87% the next because getEffectiveDailyTargetMin fell through to the current settings value. A new backfillDailyTargetInArchives stamps the current target onto any history row missing one, with a _targetBackfilledAt marker so the UI can dim or footnote those numbers if it wants to (backfill is a best-guess; original value is genuinely lost). Idempotent, runs once per boot. (2) Untracked-meeting blocks (parcels, IT, etc.) derive completeness from the wall clock — fine for a live day, ambiguous in history. Archiving at lunchtime would snapshot a 14:00 block as "not complete" purely because the wall clock hadn't reached its end yet. Archive now sets _archivedBeforeBlockEnd: true on untracked blocks whose scheduled end hadn't passed, so history can distinguish "user skipped it" from "user archived before it ran". (3) classifyDrift now documents its boundary semantics inline: the on-track band is [-30, 30] inclusive, "slightly behind" is (30, 60], asymmetric inclusivity is deliberate (callers pass rounded driftMin; both 30m-ahead and 30m-behind read the same in everyday language so both should land on-track).

This release closes the 21-finding logic audit bundle begun at v1.8.169 — 5 releases, 21 findings, distributed across cloud-snapshot races, tracker semantics, settings + category lifecycle, races + UX, and wall-clock time. Track record for the audit is now clean.

v1.8.172

Send-all: inline silent refresh on 401, reconnect banner; verify-after-send; preparer overwrite prompt names the previous queuer; silent-refresh watchdog

Four race/UX fixes from the logic audit's concurrency cluster. (1) A 401 mid-Send-all now attempts one inline silent token refresh before bailing — if Google grants a fresh token, the failed row retries and the loop continues. If the refresh fails or it's already been tried once this loop, the remaining rows get annotated "not sent — reconnect to resume" and a sticky banner appears above the pending list with a one-click Reconnect button. Previously the user had to remember which rows succeeded and re-open Settings to reconnect. (2) Every sendTrackerAdjustment now performs a verify-after-send round-trip — reads the count cell back and compares it against what we wrote. If they don't match, the row's status surfaces "sent · cell drifted to N (concurrent edit?)" so a two-device race or a manual mid-write edit is visible rather than silent. (3) The preparer-replace confirm modal now identifies who queued the existing allocation, how many quotes, and how long ago — and distinguishes "your earlier queue" from "another preparer's queue". Previously the prompt said "queue waiting from earlier" without naming the source. (4) The silent-refresh flag now has a 30-second watchdog: if the GIS callback never fires (browser kills the iframe, third-party cookie block, GIS internals stall), the flag clears so the next real user-driven auth failure surfaces the modal instead of being swallowed.

v1.8.171

Category overrides keyed by name+minutes, settings-merge race fix, missing re-flag clears skip, daily-target reset note

Four lifecycle fixes from the logic audit's settings + category cluster. (1) Category overrides are now keyed by name + minutes instead of name alone — same item name at a different per-line minutes value is a different category-tier candidate (e.g. "Canon 50mm" at 16m = Pro Lens, at 9m = Entry Lens), and the old key collapsed both into whichever was set first. guessWexCategory still falls back to legacy name-only keys, so existing overrides keep working until they're re-saved. (2) The cloud-settings listener now merges categoryOverrides rather than blind-replacing — fixes a race where a boot-time migration of legacy localStorage overrides could be wiped by a cloud snapshot arriving before the migration's saveSettings() landed. (3) Re-flagging an item as missing after Discarding its pending tracker row now clears the skipMissingTracker opt-out — previously the second "missing" event silently never reached the audit panel. Applied to both the top tile button and the context-menu action. (4) The Pull-from-tracker confirm modal now explicitly notes that the daily-target override "clears at archive" so the lifecycle is visible at the point of the action.

v1.8.170

Tracker semantics: revised filter, locale-tolerant dates, split-quote re-point, sent-only diff, preparer skipped feedback, hashed storage keys

Seven semantic fixes from the logic audit's tracker-integration cluster. (1) revised: true changesLog entries (per-item minutes corrections from mergeInput) are filtered out of the pending tracker writes panel — the Wex Adjustments schema only models integer line counts per category, so sending a revision was inflating the line count by +1 for what's really just a budget tweak. (2) findTrackerDateBlockRow now tries DD/MM/YYYY, MM/DD/YYYY, YYYY-MM-DD, DD-MM-YYYY, MM-DD-YYYY and DD.MM.YYYY so a locale change on the shared tracker workbook no longer silently kills sends. Error message also clarified. (3) Splitting a quote now re-points state.trackerReversals entries to the new qid alongside the existing changesLog.added re-point — previously a pending undo would send to the OLD qid's column and strip the OLD qid from its Quote Number cell. (4) New reversals carry itemId so future moves can re-point exactly; old reversals still match by qid+name. (5) The "Manual adjustments diff" panel now counts only SENT changesLog entries and SENT missing items — pending writes already live in the panel above, so counting them in the diff was double-surfacing them as "tracker is behind" mismatches. (6) Preparer paste path now wires the parser's skipped-line feedback into the empty-result modal and (when work parses) into the same toast that owner-mode uses — a paste with mostly-skipped lines no longer reports a small count without explaining why. (7) SHEETS_TOKEN_KEY and PRODUCT_LOOKUP_SHEETID_KEY now include a short hash of the OAuth client ID. If the client ID ever rotates, the old tokens become orphaned slots — no more silent 401s on first API call after boot. One-time migration moves the legacy slot value across.

v1.8.169

Tracker bookkeeping: snapshot races + missing-removal reversal + archive trace

Three correctness fixes from the logic-audit pass. (1) Cloud snapshot race: stamping sentToTrackerAt during a Send-all loop could miss when Firestore replaced state mid-loop — the stamp landed on an orphan object and the row reappeared in pending writes. The send path now resolves the source entry against live state by stable key (itemId for added entries, item.id for missing items, composite key for reversals) and stamps both the live and the captured reference. (2) Missing → Remove reversal: removing an item that had been flagged missing and already sent to the tracker now queues a reversal — previously the "-1" stayed on the tracker for a line that no longer existed in the app. The undo path symmetrically removes the queued reversal if the deletion is rolled back. (3) Archive trace: state.trackerReversals is now snapshotted into each day's archive entry so history retains a record of writes that needed undoing. A fresh day starts with an empty reversals queue — any pending unsent reversals are frozen in the archive of the day they belonged to.

v1.8.168

Source indicator + item-state inline legend

Two clarity additions. (1) The expanded Workload journey now shows a "added sources" row at the bottom — small chips counting how today's added entries break down: manual / duplicate / merged / backfilled. Surfaces the multi-channel reality (e.g. "+2 manual · +1 merged") without having to scan the per-item list. (2) The Today empty state ("No allocation loaded · Paste rows…") now includes a short inline legend explaining each item state — inspect / nova / admin / missing / declined — with the % weight and what kind of control each uses. Seeds the mental model before the first item ever loads.

v1.8.167

Metric tooltips with explicit denominators

Hovering or tapping any of the four Status-view cards (Progress / Pacing / Projected end / Items) now shows a tooltip that spells out exactly what's being measured and what the denominator is — "Progress = inspection time worked / daily target. 100% = on target. Partial work credits item-progress %." The Workload journey summary card on Today/Status/Stats also explains itself: currently-loaded minutes vs target, click to expand the trip-computer. Owner Stats cards (Quotes done / Today's progress / Pacing) get the same treatment. Closes the "four overlapping metrics with similar names" clarity issue from the in-app vocabulary review.

v1.8.166

Audit reads the tracker's Adjustments tab too

The Audit now reconciles against the tracker's Adjustments tab in addition to Daily Allocation and Allocation Q. For the audited date, it reads the 5-row adjustments block (added counts row, missing counts row, Quote Number row across all 12 category columns), groups the app's changesLog and missing-items by category, and surfaces per-category differences. A new "Manual adjustments diff" section appears below Day target check when categories differ — listing added/missing count mismatches and qids that exist only on one side or the other. Catches manual spreadsheet edits the app doesn't know about, and surfaces unsent app changes that haven't reached the tracker yet. Wired on both the live Audit tab and the per-day History audit modal.

v1.8.165

Live Audit: pull-from-tracker on day-target rows

The live Audit tab's "Day target check" now mirrors the History audit modal: mismatched Time allocation and Time target rows get a "pull from tracker" button alongside the new tracker-vs-app delta. Click → confirm → app pulls the tracker's value into state and re-renders. Time target writes to state.dailyTargetOverrideMin (today-only override; the global setting in Settings → Workday → Daily target stays untouched). Time allocation writes to state.originalAllocationMin so the workload-journey snapshot agrees with the tracker. The day-status strip and workload hero re-render alongside.

v1.8.164

Tracker reversal: qid cleanup + dedupe

Two related fixes to the qid handling in the tracker's Quote Number cell. (1) Sending a reversal now strips the qid if there's no other sent-and-not-reversed write for that qid — so if you mark missing, send, then undo, the qid leaves the cell. If the quote has multiple changes and you undo just one, the qid stays because other writes still need it there. (2) The qid cell is now deduped on every write — a qid appears at most once even if manual edits or legacy writes left it duplicated. Forward writes always add the qid (dedup'd via Set); reversals walk the remaining sent-write state to decide whether the qid is still earned.

v1.8.163

Clarity pass: Audit help, heatmap legend, By category, viewer terminology

Four small clarity items in one bundle. (1) An info-button on the Audit toolbar opens a one-screen explainer of every chip, action, and section in the tab — discards what each thing means without having to remember. (2) The pace heatmap legend now reads in the same percentage notation the cells use ("faster than plan ≤−5%", "on plan ±5%", etc.), matching the rest of the app. (3) The Strengths tab is now labelled By category in the nav and in the manager view — much more obvious what's inside. (4) Settings sub-head bridges the "viewer" vs "manager" terminology — "Viewers — typically your manager; signs in, sees your dashboard, can't change anything".

v1.8.162

History audit: "why might Time allocation differ?"

When a day's Time allocation mismatches between app and tracker, the audit modal now expands a "Why might this differ?" diagnostic block below the four-field check. Walks the day's items, compares each one's per-item time against its likely Wex category budget, and lists the items whose times don't match — sorted by largest contribution, capped at 8 with a "+N more" tail. A summary line tallies the sum of those deltas against the tracker-vs-app gap, so you can see how much is explained by per-item mismatches and how much is "unexplained" (typically mhall added or revised lines in the tracker after first paste). Plus a meeting/untracked-block context note when present, calling out that those don't count toward Time allocation but can feel like part of the gap.

v1.8.161

Settings modal: 80vw wide + main-tab style jump-nav

Settings modal now opens at 80% of the viewport width (capped at 900px) so content has room to breathe on a desktop. The sticky jump-nav at the top of the modal now mirrors the main page tab bar: mono uppercase labels, larger padding, accent-coloured underline on the active section, subtle hover underline on the rest — instead of the previous pill-row look. The clicked section gets the active underline; defaults to the first section so the nav never looks stateless.

v1.8.160

History audit: surface the concrete delta

Every mismatched row in the Day target check now shows the exact gap in the same units as the row — e.g. "tracker has 12m more than app" or "tracker has 1 less than app". No more squinting at two formatted values to figure out which way you're off. Above the changes list, a one-line summary shows N added with sent / unsent count chips and N removed, so a glance tells you what's outstanding for the day before you scroll any rows.

v1.8.159

Settings modal: real declutter

Substantial cleanup of the Settings modal. 13 sections collapsed to 8: Viewers and Preparers folded under one "Sharing" header (with light sub-heads), Google Sheets and Product Lookup combined into one "Tracker · Google Sheets" section, Diagnostic absorbed back into "Data" as a third action button next to Backup and Restore. Every explanatory paragraph trimmed to one short sentence or moved into a tooltip; the verbose multi-line `settings-stats` blocks replaced with a cleaner `.settings-hint` style. Meds-reminder toggle and its time input now share a single row instead of being stacked. Spacing between sections tightened. The bottom "Signed in as X" line removed — the badge at the top of the modal already shows it. Jump-nav shortens correspondingly to 8 entries; everything still scrolls and all element IDs are preserved so existing wiring is untouched.

v1.8.158

History audit alignment tools

The per-day audit modal in History now has tools for aligning the app's archived day back to the tracker when they disagree. Each mismatched row in the Day target check that maps to an editable archive field gets a "pull from tracker" button — click it, confirm, and the app's value (daily target or originally-allocated time) is overwritten with what the tracker says. Underneath, every entry in the "Changes recorded that day" list gets a sent-stamp toggle: a green check means "marked as sent to tracker", a grey circle means "not sent". Plus bulk header actions: Mark all sent (N) to stamp every unsent entry in one go, or Clear all sent stamps (N) if you need to re-send. Total Lines and % of Target are derived, so they don't get align buttons — you'd fix them by adjusting the underlying allocation/target.

v1.8.157

Settings modal: clarity pass

Smaller cleanup of the Settings modal layout and copy. New "Signed in as X" badge at the top of the modal body so you can confirm whose account you're configuring without scrolling to the bottom. Tightened the verbose "viewers and preparers need a Firebase Auth account" warning. Split the Diagnostic snippet out into its own section instead of being pinned awkwardly to the bottom of Data — the Settings jump-nav now lists it as its own destination, and the section header makes it less likely to be mistaken for "developer-only".

v1.8.156

Per-day audit in History

Each archived day in the History tab now has an "Audit" button next to Timeline and Delete. Click it for a modal that runs the same four-field tracker check against that day's Allocation row (Total lines, Time allocation, Time target, % of target — app value alongside tracker value, with a green/amber/grey status per field). Underneath, the day's recorded changesLog renders with source chips (manual / merged / duplicate / backfilled) so you can see what was added or removed retrospectively. Lets you spot historical drift without leaving History.

v1.8.155

Status-light tooltip: Firebase + Google Sheets state

Hover (or focus / tap on touch) the blinking sync indicator in the top corner — the tooltip now shows the full picture: Firebase sync state on the first line (synced 2m ago / pending / syncing… / sync failed / offline) and Google Sheets state on the second (connected with token TTL in minutes, or not connected, or sheet ID saved but disconnected). Updates live every 10 seconds along with the existing "synced Xs ago" ticker.

v1.8.154

Discard button on pending tracker writes

Every row in the Audit pending-writes panel now has a small × discard button next to Send. Confirm modal explains the semantics: "the app's own data stays as it is, only this pending tracker write is dropped". For an accidental add (+1 row) it splices the changesLog entry; for a missing-flag (−1 row) it sets a skipMissingTracker flag on the item so the missing state stays in the app but doesn't send; for a reversal it splices from the reversal queue. Use for test runs, accidental ticks, or anything you don't want recorded in the tracker's Adjustments tab.

v1.8.153

Forgot password, hide-don't-disable Next arrow, scrollbar polish

Cleanup pass on three pre-existing audit items. (1) A "Forgot password?" link under the Sign in button — types your email, click it, and Firebase emails you a reset link if an account exists. Locked-out colleagues and preparers now have a self-service path that didn't exist before. (2) The Next-day arrow on the day navigator is now hidden on the live Today view instead of rendering disabled — a permanently-greyed arrow next to a live one read as broken, and the separate Today button already handles "back to today" so the disabled Next was just noise. (3) Removed the stray 3px border-radius on the modal-body scrollbar thumb — small thing, but the visual language is sharp corners everywhere.

v1.8.152

Auto-archive snooze, search-jump a11y, touch tooltips

Three accessibility / "still annoying" fixes. (1) The post-workday auto-archive banner now has a "Snooze 1h" button alongside Archive and Dismiss — so when you're not ready to archive yet at 17:00 you can hide it for an hour without losing the reminder for the day. (2) The search-jump overlay (the / shortcut) now uses the same modal class names as the rest of the app, so it picks up role=dialog, focus trap, focus restore, and a proper close button — was previously the one modal-like surface the a11y wiring missed. (3) Tooltips were hover-only — useless on touch and invisible to keyboard users. They now also show on focus (instant, no delay), and on tap on touch devices (tap to show, tap again or elsewhere to hide). Important for mhall using preparer mode on a phone.

v1.8.151

Pace heatmap: consistent notation + readable cell text

The pace-by-time-of-day heatmap was the last surface still showing pace as the raw multiplier "0.95" / "1.20" while every other view (history chips, stats bars, status cards) said "−5%" / "+20%". Cells now use the same short formatter, so the same metric reads the same everywhere. Also fixed the cell text contrast — dark numbers were unreadable on the mid-tone info-blue (on-plan) and danger-red (slow) backgrounds; those two states now use light text, while the bright lime and amber cells keep the dark text they always had.

v1.8.150

Manager tutorial rewrite + Projected end overrun label

Two manager-facing fixes. The Manager tutorial modal now describes the Status tab — which is what the manager actually lands on — rather than the day-status strip and Today-tab hero they never see. New section breakdowns mirror the real page in order: Pacing pill, Now, Today, Workload, Timeline, Active flags, Last day / This week / Patterns. And the Status "Projected end" card now flips between green (15+ min early), amber (15+ min past 16:30), and red (60+ min past) with a "≈Xm past 16:30" sub-line, matching what the tutorial copy says. Previously the card silently rendered the time with no visual cue regardless of how far off it was.

v1.8.149

Audit-tab badge, auto-refresh, Settings jump-nav

Three small UX wins. (1) Audit tab badge — a small warn-coloured count chip next to the Audit tab label whenever pending tracker writes exist, hidden when zero. You can see at a glance how many writes are queued without switching tabs. (2) Audit auto-refresh on tab open — first time you open Audit per session (or whenever the cached fetch is more than 10 minutes old), the tab kicks a refresh automatically instead of waiting for you to hit the button. (3) Settings jump-nav — a sticky row of section links at the top of the Settings modal. The 12 sections (Workday, Theme, Sharing, Sheets, etc.) are now one click away regardless of where you've scrolled. Built dynamically from the DOM so new sections appear in the nav automatically. Closes a long-standing UX audit item.

v1.8.148

Send all, EOD warning, synced category overrides

Three small but useful additions to the tracker-writes flow. (1) Send all button on the Audit pending panel — confirms then walks each row sequentially, skipping rows without a category picked. (2) End-of-day warning in the EOD modal whenever pending tracker writes exist, with an "Open Audit" jump button — doesn't block archive (the queue survives), just stops the silent "I forgot to send" case. (3) The category-override memory that learns your "Canon EOS R5 → All Cameras" picks now syncs via settings to Firestore, so your overrides survive across devices and browser cache clears.

v1.8.147

Wex Product Lookup catalogue sync — wider autocomplete

New "Product Lookup catalogue" section in Settings → Google Sheets. Paste the Wex Product Lookup workbook URL, sync it, and the manual "+ Add item" autocomplete now sees the whole catalogue, not just items you've inspected before. Catalogue items show with a small "cat" badge and carry their Parent SKU — minutes aren't on the catalogue so the time input stays at the default. Stored locally per device, refreshes weekly when you open the app. Reuses the existing Sheets OAuth token (no new scope needed).

v1.8.146

Day-target audit: tracker totals vs app totals

New "Day target check" section in the Audit tab. Refresh now pulls both the Daily Allocation tab (the per-item view) and the Allocation tab's row for today (the aggregated view) in parallel. Compares four fields against the app's own numbers: total lines, time allocation, daily target, and % of target. Each field gets a green check, amber warning, or grey "missing" depending on whether app and tracker agree (with tight tolerances for time rounding and percentage display). Either tab failing doesn't block the other — partial data still renders.

v1.8.145

Tracker reversals + queue toasts

Three fixes to keep app and tracker in sync when you change your mind. (1) Adding or marking-missing now fires a brief toast — "Queued: +1 Pro Lens" or "Queued: −1 Compact Camera" — so you know the Audit pending panel has work without having to switch tabs. (2) If you remove an item or unmark a missing flag after already sending it to the tracker, the app now queues a reversal write (a −1 to undo the +1, or a +1 to undo the −1) which shows in the pending panel with an "undo" badge — send it to bring the tracker back in line. (3) Sent stamps are cleared when you leave a state, so the cycle mark-missing → unmark → re-mark-missing now correctly queues each new event instead of silently dropping it.

v1.8.144

Silent Google Sheets token refresh

Google's OAuth access tokens expire after about an hour by policy. Until now you'd have had to manually reconnect every hour. The app now silently refreshes the token in the background five minutes before it dies — no consent prompt, no popup, no notification, as long as you're still signed into Google in your browser. If you've signed out of Google or revoked the app's access, the silent refresh fails quietly and the Settings panel flips to "Not connected" so you'll see it next time you look, but no modal interrupts whatever you were doing.

v1.8.143

Write adjustments back to the Wex tracker

Phase 3 of the Sheets integration. When you manually add an item to a quote in the app, or mark one as missing, the Audit tab now shows a "Pending tracker writes" panel with each pending adjustment. Each row lists the qid, item name, +1 or -1, and the suggested Wex category (the app cross-references the app's own item bucket against per-line minutes for the 12-category enum). You confirm or change the category from a dropdown, click Send, and the app writes the count to the date row and the qid to the Quote Number row of your Adjustments tab — leaving the Time Adjustment formula rows untouched. Idempotent: each entry stamps a "sent" timestamp on success so re-clicking is safe. Multi-qid in the same category comma-appends. Category overrides per item name are cached so a once-classified item never asks again. One-time OAuth re-consent needed for the wider read+write scope.

v1.8.142

Audit tab — cross-check today against the Wex tracker

New owner-only Audit tab in the main nav. Hit Refresh, it pulls the tracker's Daily Allocation tab via the Sheets API and diffs the rows against the app's current state.quotes. Each quote becomes a card colour-coded by status: match (green) when the items and times agree, mismatch (amber) with a per-item list of what differs, app-only (blue) when the qid is on today but not in the tracker, tracker-only (red) when the tracker has a qid that isn't on today. Time differences show both sides side-by-side. Header summarises issue count, last-sync time, and a refresh button.

v1.8.141

Google Sheets OAuth plumbing

Phase 1 of the Wex-tracker cross-check feature. Settings now has a "Google Sheets" panel where you can Connect (OAuth via Google Identity Services, read-only Sheets scope), save the tracker's sheet URL or ID, and run a Test that fetches the workbook's title and tab names. The app stores the access token client-side with a 1-hour expiry and never writes back to the sheet. No audit/diff logic yet — that's the follow-up release. This one exists so we can confirm the consent flow actually works through Wex's Workspace before building on top of it.

v1.8.140

Untracked blocks now say "auto-counted, no action needed"

Morning parcels and other untracked blocks (Required / no-credit, or authorised untracked time) never needed a Mark-done click — the Mark-done button simply wasn't rendered, and their full duration is added to the day automatically at archive. But the absence of a button was the only signal, which was easy to read as "I missed something". Untracked blocks now show a small static "check_circle · auto-counted · no action needed" indicator in the slot where Mark done would normally appear, so the positive cue is visible rather than inferred.

v1.8.139

Added-item provenance: EOD source chips + history persistence

Items added after the day's initial allocation are now tagged with how they arrived — manual (typed in via the + Add affordance), duplicate (copied from another item), merged (came in via a later paste), or backfilled (retroactively flagged via the context menu). The EOD modal's "Today's allocation changes → Added" rows show a small chip per source so you can tell at a glance which extras were boxes you opened versus supplier additions. The archive snapshot now also preserves the day's full changes log — previously dropped at archive time — so History will, going forward, show "+N extras · −M dropped" on each day and a "Changes today" section in the expanded view with the same source chips. Older archived days have no log to display.

v1.8.138

Preparer polish — date context, item-level edit, smarter review

Five small improvements after testing the preparer flow end-to-end. The preparer view now shows today's date and weekday under the header so it's hard to queue the wrong day's allocation by accident. The review step gained per-item delete (×) — useful when the source list has one item that shouldn't be there, without having to scrap the whole quote. On your side, the review modal now defaults to all-ticked with Select all / Clear all toggles (the typical case is merge everything), and rows whose qid already exists in today get a small "already in today" badge so you can spot when items will fold into an existing quote. Timestamps everywhere now read "12 minutes ago" instead of locale strings.

v1.8.137

Preparer mode — guided three-step workflow

The preparer UI is now a proper guided flow: paste → review → queue. After parsing, the preparer sees every quote with its items and times, can remove any that shouldn't be sent, and watches running totals. A step-progress strip shows where they are. First-visit onboarding explains the three steps, and a Help button in the header brings the explanation back any time. The page reacts live to the owner picking up the queue, so the preparer can see when their work has landed.

v1.8.136

Preparer role — let someone else queue your day

A third access role alongside owner and viewer. A preparer signs in with their own credentials and gets a stripped UI: just a paste box and a queue button — they can't see your day, tick items, finish quotes, or change anything. When they queue a paste, you see a banner with a Review button; the review modal lets you pick which quotes to merge into today, queue the whole paste for tomorrow, or discard it. Manage preparers in Settings under Sharing. The Firestore security rules restrict preparers to a single inbox document.

v1.8.135

One set of pacing thresholds

"Pacing" — how far ahead or behind the day's plan you are — was being judged by four different sets of cut-offs across the app. The Status pill could read "On track" while the Pacing card right below it said "23m behind", both from the same number. There's now a single shared rule: 30 minutes behind shows amber, 60+ shows red, and anything inside a half-hour either way reads as on track. The day-status strip, the owner Stats card, the manager's Status pill and card, and the viewer Stats surfaces all use it, so they can't disagree anymore.

v1.8.134

End-of-day carry-forward, neutral workload copy

The "carry to tomorrow?" checkboxes in the end-of-day modal no longer come pre-ticked — carrying a quote resets its progress to zero, and pre-ticked boxes meant a fast archive did that silently. Each row now shows how far the quote got, and you tick the ones you actually want re-queued. The workload-journey status line is now plain ("45m short of target" instead of a shouted "ASK FOR MORE WORK"), and the manager's Stats view no longer shows owner-only "pick up work" prompts. Also: bulk "mark all done / Nova / admin" now stamp a start time, so a quote can't end up resolved-but-unfinishable.

v1.8.133

UX audit bug fixes

Three fixes from a full UX audit. Confirming a delete from inside Settings (a library entry, a viewer) no longer wipes the Settings panel — the confirmation dialog now opens as its own layer. Viewer mode now hides the Delete, pull-forward, and print buttons on the History tab, so a manager can't accidentally alter their view. And the active-quote highlight, resume chip, and now-line no longer vanish when you work past 16:30 — a started, unfinished quote stays active whatever the clock says.

v1.8.132

Wider modals on desktop

Content-heavy modals — settings, reference, EOD, queue-tomorrow, library browser — were sized for laptop and mobile and felt cramped on a wide screen. On displays wider than 1024px they now scale up a tier. Small yes/no confirm prompts stay compact.

v1.8.131

Rewrote the app.js table-of-contents

Internal housekeeping, no user-facing change. The map at the top of the main code file had drifted thousands of lines out of date and actively misled. Rewrote it to use copy-pasteable section names instead of line numbers, which were the part that kept rotting.

v1.8.130

Count-up on Status metrics

When the Status tab opens, the headline percentages — today's target hit, this week's average — roll up from zero instead of snapping into place. This was the last of the picked gallery animations to be wired in.

v1.8.129

Gallery-picked animations wired in

Seventeen animations picked from the curation gallery, attached to real moments: banners fade in, added item rows rotate in, removed rows and deleted quotes animate out, day-navigator steps slide directionally, a fully-done item radiates a ring, jump-to-item sweeps a highlight border, the sync dot pulses while syncing, buttons ripple on press, and a quote FINISH pops a small particle burst. All of them no-op under reduced-motion.

v1.8.128

Release notes page

This page. A standalone, reverse-chronological log of every shipped version, grouped by month and day, linked from the version number in the app header.

v1.8.127

FLIP reorder, spring progress bars, confetti burst

Quote cards now slide to their new positions when the schedule reorders — autosort, start-now, pin — instead of snapping. Progress bars ease between values rather than jumping, and a confetti burst fires the first time the day target is crossed. All three respect reduced-motion settings.

v1.8.126

Header overspill fix

The day-status strip was collapsing to a single column and inflating the header to roughly 228px tall. Corrected the flex sizing so it stays a proper five-across row between the brand and the clock.

v1.8.125

Day navigator polish

Fixed the navigator strip and read-only banner scrolling out of view on every step back, deduplicated the date label, and made past-day cards visibly read-only. Added left/right arrow keys for stepping through archived days.

v1.8.124

History days default to collapsed

The History tab now opens as a compact list of day-summary rows — expand the ones you want to dig into. Which days you've expanded persists across reloads.

v1.8.123

Per-tab scroll memory

Each tab now remembers its own scroll position. Switching from Today to History and back returns you to exactly where you were, instead of stranding you mid-page.

v1.8.122

Reference phrases — labels and seed set

Quick phrases gained an optional label for grouping, and a standard set of inspection phrases (odour, optics mite, battery wording by brand) is seeded in on first use.

v1.8.121

Inspection reference modal

A new Reference button in the sidebar opens a modal with the shutter-count grading table and your own one-tap-copyable quick phrases for findings you write often.

v1.8.120

Undo toast for accidental unticks

Unticking an item phase now shows a one-tap Undo (also via Ctrl+Z) that restores the item's exact prior state — including the tick timestamps the inspect/nova cascade would otherwise wipe.

v1.8.119

Collapsible history days & Today-style past-day view

History days collapse and expand on a click of their header. The day navigator's past-day view now renders read-only quote cards in the same visual idiom as the live Today view.

v1.8.118

Context-awareness pass

The Archive button dims when there's nothing to archive and gains an accent glow at end of day. New schedule blocks default their start time to now, and the resume chip pulses once a quote has been paused 25 minutes or more.

v1.8.117

Trim decline reasons

Narrowed the decline-reason dropdown to Mould, Heavy mould and Other — the reasons in actual use, with more easy to add later.

v1.8.116

Decline-reason modal & context-aware menus

Declining an item now captures a reason via a dropdown plus an optional note, preserved into history. Right-click menus hide actions that aren't available right now instead of showing them greyed out.

v1.8.115

Merged "Start now" and "Make active now"

The two near-identical menu actions became one context-aware "Start now" — it stamps the start time, pulls the quote to the front of the queue, and auto-pauses anything else running.

v1.8.114

Empty-state messages for Stats sub-tabs

A Stats sub-tab with no data yet — the "Items & quotes" tab was the worst offender — now shows a short explanation of what fills it instead of rendering blank.

v1.8.113

Mousewheel zoom & meetings count as worked time

Plain mouse-wheel now zooms the timeline (shift-wheel pans). Regular meetings now count toward the day's worked total once marked done, the same as untracked blocks — a long meeting no longer reads as time behind.

v1.8.112

Modal close-on-drag & arrow-skip fixes

Modals no longer close when you drag-select text inside an input and release over the backdrop. The meeting-duration field steps by one again, and focused number fields no longer change value on accidental scroll.

v1.8.111

Queued-paste & template-day parse fixes

Fixed tomorrow's queued allocation being silently dropped on the next-day boot, and stopped a template-only day from triggering an unnecessary "archive first" prompt when you paste a fresh allocation.

v1.8.110

Icons across tabs, sidebar and Status sections

Material Symbols glyphs added to the tab bar, the sidebar action buttons, and the Status view's section titles, for faster visual scanning.

v1.8.109

Load missing icon glyphs

Five icons were rendering as raw text because they weren't in the font request — added them so they show as proper glyphs.

v1.8.108

Day navigation on the Today tab

Prev/next arrows at the top of the Today tab let you step back through previously archived days inline, without switching to the History tab.

v1.8.107

Queue tomorrow's allocation any time

Added a standalone "Queue tomorrow" button in the sidebar, so you can stage tomorrow's allocation whenever you have it — not only during the end-of-day archive flow.

v1.8.106

Polish pass

Widened the Ctrl+wheel zoom hit area on the timeline, replaced the history sparkline's raw browser tooltip with a styled one, and unified the pace-chip colour thresholds across the app.

v1.8.105

History overhaul & queue-tomorrow at archive

History rows gained friendly date headers, metric tiles, per-day pace sparklines and week dividers. The end-of-day modal gained a section to paste and stage tomorrow's allocation.

v1.8.104

Timeline rewritten in HTML/CSS

The SVG day timeline distorted at every zoom level. It was rebuilt with HTML/CSS positioning, proper lane headers, sharp text at any zoom, and a popover that can't be clipped.

v1.8.103

Post-lunch resume hint & time-pill warning

A once-a-day banner offers to resume a quote left paused over lunch. Editing a quote's start/finish time now warns if the change would leave item ticks stranded outside the window.

v1.8.102

"In-between" reframed as Buffer

The in-between-time counter became "Buffer" with an explicit budget — workday minus lunch minus target — so an hour of transitions reads as budgeted time, not wasted time.

v1.8.101

Premiere-Pro timeline, pause-midnight fix, ready-to-finish nudge

The day timeline became a multi-track layout with clickable events and a detail popover. Fixed pause time being lost when a pause spanned midnight, and added a sidebar nudge for quotes whose items are all done but not yet finished.

v1.8.100

Pin the new-version toast on screen

The "new version available" prompt was inheriting the standard toast auto-fade and disappearing after two seconds. It now stays put until you click Refresh.

v1.8.99

Viewer-mode pass

Added a Patterns section to the manager's Status view and surfaced the day timeline in viewer mode. Renamed confusing labels — "Drift" became "Pacing", "Flagged" became "Missing".

v1.8.98

Pause-time chart fix & animated brand-mark

Two manager-facing charts were still counting paused minutes as worked time — fixed. The logo's three bars gained reveal, loading and hover animations.

v1.8.97

More animations

Added quote-start glow, the Strengths bar fill, sub-tab cross-fades, a resume-chip slide-in, modal open animations, and a pause/resume transition.

v1.8.96

Pace ratios as percentages

"0.7×" became "30% faster" throughout the app — a directional percentage is clearer to read at a glance than a bare multiplier.

v1.8.95

Timeline on history rows, zoom and pan

The day timeline became available per archived day in History, with zoom controls and drag-to-pan.

v1.8.94

Per-tick timestamps & the day timeline

Every checkbox tick now records a timestamp, which feeds a new editing-timeline-style view in Stats showing where the day's hours actually went.

v1.8.93

Partial credit, drift-aware projected end, safety nets

The sidebar Progress now counts partial item work instead of all-or-nothing. Projected end-time factors in current drift, a boot-time safety net re-applies the daily template, and an in-between-time counter was added to the header.

v1.8.92

What's-next suggestion

A data-derived banner suggests which specific item to tackle next, based on your per-category pace from Strengths crossed with the current time of day.

v1.8.91

Completion fanfare

The Motion One library was added and celebration animations now fire on real completion moments — item ticks, quote finish, day target hit. The earlier no-celebration rule was revised at the user's request; notifications, sounds and streaks remain off the table.

v1.8.90

Mobile day-status & defect aggregation

The at-a-glance header strip now wraps to its own row on narrow screens instead of disappearing entirely. Stats gained a breakdown of inspection findings by defect type.

v1.8.89

Morning brief, inspection findings, library browser

A day-start brief banner shows quote count, target and recent pace. Items can be tagged with a finding type, and a searchable browser for the item library was added.

v1.8.88

Bump muted text contrast

Raised the two muted grey tones after feedback that the greyish text was hard to read — both now clear WCAG AA against the background and surfaces.

v1.8.87

Live workflow pack

Added live elapsed pace on the active quote, a floating resume chip that follows you across tabs, quote-level notes, and paused-time stats in the end-of-day recap.

v1.8.86

Modal accessibility

Keyboard and ARIA support added across every modal in one pass: dialog roles, focus trapping, Escape-to-close, and focus restored to where it was when the modal opened.

v1.8.85

Owner Stats sub-tabs & pause-time pace fix

The owner's Stats view was split into scannable sub-tabs. Pace ratios now subtract paused time, so a quote that was paused doesn't read as having run long.

v1.8.84

History heatmap & multi-step undo

A twelve-week heatmap calendar was added to the top of the History tab. A multi-step undo stack with a toast and Ctrl+Z now covers destructive actions.

v1.8.83

Viewer Stats sub-tab navigation

The manager's Stats view — previously one long scroll — was compartmentalised into Now, Concerns, Trends and All-time sub-tabs.

v1.8.82

Lunch block, status ribbon, pinned quotes

Lunch now shows as a block on the time-rail at its actual slot. A day-status ribbon was added, and quotes can be pinned to stay at the top of the list.

v1.8.81

Settings & History visual refresh

The long Settings modal gained section dividers, and History rows were promoted toward a cleaner card-style layout.

v1.8.80

UX polish pack

Charts now recolour when you switch theme, plus four other small UX items cleared from the backlog.

v1.8.79

Viewer preview button & WCAG AA contrast

The owner can now preview the manager's view without switching accounts, via an eye-icon button in the header. Text contrast across the app was raised to meet WCAG AA.

v1.8.78

Quick wins pack

Dead-code removal and small clean-ups, including a fix that stops the recap card arming its auto-dismiss before a real save has happened.

v1.8.77

Important bug-fix pack

Ten independent correctness fixes from the audit's "Important" tier, spanning the sync, archive, merge and split flows.

v1.8.76

Removed the Compare tab and Elo system

The subjective pairwise-ranking Elo machinery was dropped — the new Strengths tab derives the same "what you do best" insight from real archived history instead.

v1.8.75

Archive safety net

Added a boot-time rescue for un-archived stale days, a guard against merging into a mis-dated day, the archive nudge on by default, and a visible upload progress indicator. Addresses reported data loss from un-archived days.

v1.8.74

Split into separate CSS / JS files

The single-file build was split into app.css, app.js and print-helper.js — making the codebase navigable and allowing a tighter Content Security Policy.

v1.8.73

XSS hardening pass

Escaped untrusted values rendered into the History view's HTML and removed inline event handlers — closing the highest-impact items from a security audit, important because manager (viewer) sessions read data from another user's cloud documents.