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.
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).
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).
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.
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.
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".
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.datestring 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.
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.
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).
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).
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 quotes — q.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 backfillbackfillOrphanChargingOnCompletedQuotes() 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.