Skip to main content

Changelog

Every shipped change to KVICK.BIKE — the clean-room bike-vertical Hub on Cloudflare Workers + D1. Source of truth: CHANGELOG.md in the kvick-bike repo.

Versioning re-baselined 2026-07-27 — positional, auto-generated from git. Tom rolled the runtime VERSION to v0.7.0 (superseding the "absurd v0.6.1000" the previous scheme was heading toward), then formalised the scheme: every code-altering commit is a version, numbered by position (v0.0.NNN, where NNN = count of code-altering commits). VERSION is generated at build time from git log by scripts/gen-changelog.mjs (a wrangler [build] hook) and imported into src/index.ts from a gitignored src/core/changelog.ts build artifact — no more hand-bumped constant. Only commits that touch src/ / migrations/ / public/ count; changelog: regen commits and no-code commits are excluded, so a version maps 1:1 to a pushed code change. Each generated entry carries a technical layer (commit subject + body, Co-Authored-By stripped) and an operator Plain: line pulled from the body (SHA-keyed curated fallback for older commits). At 697 commits → 669 versions when the scheme landed; build-info now equals the card's newest entry by construction.

Commit-subject convention: NO vX.X.X prefix (rule reversed)

An earlier iteration of this doctrine asked commit subjects to carry v0.7.NNN as a self-identifying counter. That rule is retired. The version is derived from commit position by the generator; a prefix on the subject just prints a fake second version on the card. Current convention: subjects are plain prose (imperative mood, no leading version tag). The generator numbers versions from git log --count; the subject stays clean. See memory rule feedback_changelog_commit_convention.md.

Hand-authored entries below stop at v0.7.383 (2026-07)

The dated entries on this page cover the module-build era through July 2026. Since then the runtime authority is src/core/changelog.ts at build time (generated from git log by scripts/gen-changelog.mjs), and the operator-facing changelog card in the app renders from that generated file. This hand-authored page is a narrative retrospective for the module-build era, not the live changelog — for anything after v0.7.383 read the What's-New card in the app, or run the generator locally against a fresh clone of the repo. The Bible pages that describe module features cite specific commit dates + versions inline; those are the current source of history at the feature level.

Twenty-two-version rollback (2026-07-23) — commit present, effect absent

The tree was rolled back to v0.6.929, undoing twenty-two versions. The commits from that range are still ancestors of HEAD, so git log reports every one of them as landed while the files say otherwise. Any tool a reader uses to ask "did this ship?" against git will answer yes for those twenty-two versions when the answer is no. Nothing is lost — the branch pre-rollback-v0.6.950 on origin points at d3d5804 (v0.6.951, last commit before rollback) and preserves every affected path. Re-landing any of it is a checkout of specific paths, not a rebuild. See Three Sessions § Part 2 for the full state at v0.6.954, the verified open issues, and the "what to settle first" list.

What v1.0.0 means — open (Tom's, 2026-07-23)

Earlier drafts of this page reserved v1.0.0 for "full monolith parity, running as Swicked's production". That framing assumed mimic the monolith verbatim was the design method. Tom overruled that method on 2026-07-23 — the modular build's logic is now considered superior in most instances and the monolith is a reference, not a template (see The monolith is a reference, not a template below). What v1.0.0 means going forward — whether it still marks a parity-and-cutover milestone, or something else — is an open question Tom hasn't ruled on. Do not assume either way.

The monolith is a reference, not a template​

Changed by Tom, 2026-07-23. The earlier standing rule was mimic the monolith, never invent — it was the finished design, to be mirrored verbatim, and the only permitted transformation was breaking it into modules. That rule is now retired. In Tom's words: the monolith "is not to be replicated, only referenced. Our logic is superior in most instances now."

  • Read the monolith to learn what a feature was FOR and how the shop actually used it.
  • Do not copy its markup, mechanism or data model on the grounds that it is the spec. It is no longer the spec.
  • A divergence from the monolith is not a defect. Under the old rule a parity sweep would find better logic and "correct" it back to the original behaviour — that must not happen again.

Two consequences this does NOT answer, and this documentation does not assume either way — they are Tom's:

  • Whether the overrule is forward-only, or reopens decisions already shipped because the monolith said so.
  • What v1.0.0 now means, since parity is no longer the method — see the caution admonition above.

Unchanged: the monolith is still never a data source without Tom's explicit yes or no.

Prior re-baseline attempt (2026-07-22) — superseded by the 2026-07-27 positional scheme above

The block below records the earlier attempt to continue the monolith's line at v0.6.471+. It's kept here because that framing appears in commit messages and Bible history from 2026-07-22 → 2026-07-27. As of 2026-07-27 the runtime version is generated positionally from git log and the "continue the monolith's line" framing is retired at the runtime layer (the changelog card still lists entries by positional counter).

Version-string re-baseline (Tom-directive, 2026-07-22)

An earlier attempt started the modular build at v0.7.0. That made the modular build look like a new product line running in parallel with the monolith it was actually replacing — a false framing that also made an unfinished successor read as newer than the finished system. Under the new baseline the modular build continues the monolith's line: same product, re-architected, one continuous chronology from v0.6.001 through the monolith's v0.6.470 and into the modular build at v0.6.471+. There is no v0.7.x line; a brief v0.8.0–0.8.6 excursion mid-stream folds in too. The renumber is applied positionally at render time in scripts/gen-changelog.mjs — the same mechanism that folds the 1.x block — 454 entries with zero gaps.

Git history is not rewritten. Commit subjects on the repo still read v0.7.xxx — that's the chronology of what shipped when, and it's untouched. Bible pages that cite specific historical versions (e.g. "shipped in v0.7.410") are correct citations at that layer and stay as-written. Chronology is the truth; the number is just a label — the label changed, the chronology didn't.

v0.7.x — Products module (2026-06-25)​

Re-baselined onto the prior version line. The clean build was briefly started at v1.0.0, but that over-claimed: this is a partial rebuild (Customers, Settings access-control, Products), while the in-production prior system sits at v0.6.470. A 1.x number made the unfinished successor read as newer than the finished system. The line now continues: v0.7.0 = 0.6.470 + 0.1 — same product, re-architected, honestly pre-1.0. The v1.x entries below are kept as history but were a premature reset folded into this baseline. (A second re-baseline in 2026-07 folded the v0.7.x line back into a continuation of v0.6.471+ — see the admonition at the top of this page. Chronology below is unchanged; only the labels moved.)

v0.7.383 — Till reset is "Esc" + Esc key binding (BIKE.L1-0020, 2026-07)​

The Till-header reset control on every tab (Sales / Customers / Inventory / Purchases / Service) now reads "Esc" — was the accent-coloured serif "C". The physical Esc key performs that same action on the active tab via core/keys.ts: idle Esc clicks the active module-pane's visible + enabled .till-clear-dot, so the label and the key name the same action. Function unchanged — Sales = Hold, other tabs = clear the till but preserve the data. LIFO stack still runs first (Esc closes an open dialog before it resets). The old Sales-specific #sc-cancel onEscClear default was retired; the onEscClear API stays for explicit per-tab overrides.

v0.7.382 — Settings Audit Trail ledger (BIKE.L2-0135, 2026-07)​

Read-only window on the L1 audit spine, pinned to the bottom of Settings. Renders the narrated summary from audit_events (migration 040) verbatim; server-side search + category / severity / outcome filters + date range (kbDatePicker) + click-to-sort (kbSortable) + offset Load older paging so filters / sort act on the whole history. sys.permissions.view (Owner + sys-admin). No schema change, no Core edits. See the Audit Trail ledger on the Settings module page.

v0.7.379 — AIM cutover tool + Sales audit-3 / alpha-v2 sweep + BC PST accessory rule + Park→Hold rename (2026-07)​

The 79-patch run that shipped the AIM cutover tool as a real operator surface, closed a whole audit backlog on Sales (audit-3 + alpha-v2), landed the BC PST bundled-accessory rule at Till parity, and did an owner-directed Park → Hold/Held vocabulary sweep.

  • Migration module (BIKE.L1-0016, v0.7.368). New sys-admin-only tab: src/modules/migration/ + migrations/etl/. Backup picker (v0.7.370), local extract.py → SQLite snapshot → stage.py → D1 aim_raw_* staging → converter (catalog → suppliers → customers → sales → bikes → service → purchasing → finalize) → fidelity gate BEFORE wipe → parity report AFTER convert → commit or D1 Time Travel rollback. Runner pumps one step per POST, cursor in migration_jobs. New migrations: 049_migration_tool.sql (permission + migration_jobs + migration_presets, seeded preset "Fresh: Customers + Inventory + Sales History"). Tier-1 money bugs fixed at the source (T1 #1/#4 deposit signs + deposit_kind, T1 #2 total_tax_cents in INSERT, T1 #3 return signs, T1 #5 payments.staff_id); Tier-2 shape bugs (retired mfg/categories, supplier_sku, service created_at, service_menu tax); the sad_type='S' = SERIALIZED taxonomy fix (v0.7.373, 717 bikes were being loaded as service tickets); location_id=1 pin (v0.7.372, 9,744 inventory rows had landed at re-seeded id 3). Static column-name validation on every INSERT now — the migration converter can't ship an INSERT that doesn't match live schema. New Bible page: Migration module.

  • Park → Hold / Held rename (v0.7.337, owner directive). Operator-facing labels swap Park → Hold; substrate follows via migration 047 (parked_sales → held_sales, parked_at → held_at, idx_parked_active → idx_held_active, notes prefix auto_parked: → auto_held:). Code identifiers rename in lock-step (parkActive → holdActive, resumeParked → resumeHeld, scheduleAutopark → scheduleAutohold, /parked → /held, data-parked → data-held). Paused-sale status text stays "Pending" (unified at v0.7.294) so an operator reads one word for the state; hover reads "Held by <cashier>".

  • BC PST accessory rule at Till parity (BIKE.L2-0248). v0.7.373 first shipped a per-line "Fit to bike" toggle (migration 050 transaction_lines.fitted_to_bike) that would drop PST on a lock / cage / bell fitted to a bike-in-invoice. v0.7.378 removed the toggle at the Till — attachments are ALWAYS PST + GST at the counter; the fitted-to-bike exemption is granted only via the Service Tab (not built yet). fitted_to_bike column kept but write path removed. v0.7.379 makes the rule DURABLE at the subcategory level — convert-catalog.ts sets products.default_tax_category_id by subcategory NAME (LIGHTS/FENDERS → exempt-by-category, COMPUTERS/CAR RACKS → fully taxable, LOCKS/BELLS/BAGS/PANNIER RACKS/WATER BOTTLES/BOTTLE CAGES/PUMPS → context-dependent-accessory). Tax resolver reads COALESCE(inventory, product.default, category.default).

  • Discount model correctness (alpha-v2 #1 BLOCKER). The gotcha named: per-line discounts are INSIDE the subtotal; cart discounts are OUTSIDE. transactions.total_discount_cents holds both summed. Reading total_discount_cents where you meant cart_discount_cents double-counts, over-refunds, and shows phantom balances on layaways. Formula: cart_discount_cents = total_discount_cents − SUM(transaction_discounts WHERE line_id IS NOT NULL).discount_cents. v0.7.359 fixed detail panel + hovercard + return dialog + reprint receipt + server /refund (was over-refunding by cart discount every time). v0.7.360 fixed /deposit (was computing total = subtotal + tax with NO discount logic — phantom balance on every discounted layaway). v0.7.361 fixed /deposit-update-lines (a layaway edit was re-booking at full price). Same double-count bug found in the Python ETL — sal_soldprice is already NET, converter was subtracting the discount a second time.

  • Sales audit-3 sweep (v0.7.336 → v0.7.357):

    • #1 money COLLECTED today — header value = net real-money tenders (excludes on_account / store_credit / gift_card redemptions, includes deposits); clickable → tender-breakdown popup grouping by payments.card_brand (v0.7.345, audit-3 #6).
    • #2 gift-card issuance is not a sale/revenue — ETA s.181.2 + BC PST Bulletin 311 + ASPE treat it as deferred revenue liability; /summary drops variant-less tax-exempt liability lines from count + revenue.
    • #4 restocking fee is GST-inclusive — ETA s.182: fee × 5 / 105.
    • #5 dynamic Take-Payment title — INVOICE / LAYAWAY / SPECIAL ORDER suffix, colour-keyed.
    • #9 on-account confirm — shows before / +sale / new balance before you bill.
    • #10 per-line tax badge — every cart line shows resolved tax treatment (GST, PST, GST+PST, EXEMPT).
    • #11 split-tender brand-specific card legs — option value card_present:<brand> so tender breakdown can attribute correctly.
    • #13 GC redemption placeholder — "Scan or type the card number", not the stale text.
    • #14 receipt after installment — /deposit-payment fires the same openReceiptPrompt.
    • #15 refund count in header — matches the row count on screen.
    • #16 discount reason on recalled sale + reason now optional.
  • Sales alpha-v2 sweep (v0.7.359 → v0.7.365):

    • #1 layaway honours discount (BLOCKER, above).
    • #2/#7 till line floors at zero — 0 removes; closes the negative-cart cash-payout vector (reverses direction on BIKE.L2-0215).
    • #3 in-progress PENDING row tracks live till — draft PUT branch calls loadLedger on every change.
    • #4 collected-today refreshes after deposit installment — loadSummary fires after every mutation.
    • #5/#6 discount double-count (above).
    • #10 typeahead race guard — stale search responses whose query no longer matches box are dropped.
  • Cart line UI polish (v0.7.347/.348/.374/.375/.376). Each cart line's right column is now a 2×2 grid: row 1 + Discount / unit price, row 2 − Remove / line total (right-aligned under unit price). Per-line + Discount popup takes % / $ + reason; reason lands on transaction_discounts and prints on receipt + estimate + shows on the recalled sale.

  • Take-Payment unification (v0.7.342/.343/.353). Title type-pill (mint INVOICE / cyan LAYAWAY / peach SPECIAL ORDER), purpose-drawn PAYICON SVGs per tender (no emoji), one Charge popup drives every path including installments (standalone Add-Funds modal purged). Split-tender offers brand-specific card legs.

  • Change due BIG on cash sale (v0.7.371, counter spec scenario 1) — 34-px DM Serif Display, colour-shifts green when change > 0.

  • Layaway / Special Order without a customer → auto-attach (v0.7.377). Opens Attach Customer with Keep walk-in hidden; pendingAction fires the original action on pick. Closes silent walk-in oversell on full-pay-when-out-of-stock.

  • Ledger + detail polish. Today's Held/Pending is scoped to today (v0.7.349, local hold-date). Clickable Returned pill in the detail band opens the linked refund (v0.7.350). "(click to edit customer)" hint on the ledger heading (v0.7.338/.339/.340). Header no longer leads "Till open" (v0.7.351) — reads "Cashier X · N sales today · $Y collected · N refunds".

  • Reverse payment on open deposit (v0.7.333, Tier-2 #13). /payment-reverse extended to open layaways/SOs; ledger shows reverse + refund rows; client reopens detail for re-collect.

  • Ad-hoc mid-sale discount (v0.7.334, Tier-2 #14). Manual on-Till + Discount with reason; stored on transaction_discounts with discount_type='manual'.

  • No-match create-customer (v0.7.335, Tier-2 #18). Idle bar "+ Create customer" pre-fills name from the search query.

  • Phone + email validation (v0.7.366, audit #8). App-wide. core/formats.ts mask inferred from type=tel / type=email; blur validates + marks aria-invalid; kbFmt.phoneValid / kbFmt.fieldValid exported.

  • PIN gate (v0.7.335, .358). Label reads "Enter your PIN to continue"; harden() stamps autocomplete="new-password" on password inputs so Chrome stops replaying a saved admin code (audit v2 #11).

  • Till reset "C" back (v0.7.346). Reverts the "K"-in-circle experiment (v0.7.329/.331) to the accent-coloured serif "C" across all tabs.

  • Search hint occlusion fix (v0.7.367, audit v2 #6 / v3 C). sales.search_bar scaffolding hint refuses on non-empty input; open hint retires on first keystroke; migration 048 rewords copy to phase-neutral.

  • New migrations. 047_hold_rename.sql, 048_search_hint_copy.sql, 049_migration_tool.sql, 050_fitted_to_bike.sql.

v0.7.327 — Gift cards + Account credit + Receipt engine + Tier-0/1/2 audit sweep (2026-07)​

The 91-patch run that took Sales from "Till + Charge popup at parity" to "the shop's ten audit findings closed, gift cards + account credit + misc lines shipped, receipts industrialised, scaffolding tooltips teaching the register."

  • Scaffolding tooltips engine. New Kernel primitive core/scaffold.ts + migrations 043 (per-staff tooltip_mode + staff_feature_state) + 044 (feature_registry seeded with 129 hints across every module). Per-staff On / Off / Heuristic control; decay curves by frequency_class (high 15/30 · medium 30/60 · low 8/120 · rare 4/180); idle past the decay window resets use_count to 1 (re-teach after a two-week absence, v0.7.313); keyboard-Tab-within-700 ms gate kills programmatic-focus firing (v0.7.318); pure hovercard, no ⓘ marker (v0.7.305); replaces the control's own title (v0.7.306). New Bible page: Scaffolding tooltips.

  • Gift cards + Account credit + Misc-line pill. The GC/Bulk/Misc pill (v0.7.302) opens five kinds of non-catalog line: Misc / open-price item (v0.7.298, also fires from a no-match search); + New inventory item (v0.7.299, cross-mounts Inventory hidden via kbEnsureModule); + Gift Card (bearer, scanned serial, dupe-reject); + Account Credit (retainer, customer_account_ledger); Bulk (real is_bulk SKUs). The −/+ stepper is repurposed on GC/AC lines (v0.7.307) — add-another / confirm-remove, never a qty change. Migration 046 (transaction_lines.account_credit_customer_id, gift-card serial reuses serial_number); v0.7.308 fixes estimate → sale convert via reconstructStoredLine + issueSpecialLines so a card isn't re-taxed or re-issued. Misc-line tax picker reads live TAX_CATS (v0.7.300). Add-account-credit action on the customer Account modal (v0.7.309). Per-shop tender toggles — migration 045 + f63cae3, owner-only Settings switches gate + Gift Card and + Account Credit at the pill and the customer card.

  • Receipt engine — one receipt everywhere. New architecture: Receipt engine. Post-sale prompt (v0.7.252, Print / Email-soon / No receipt, Enter=Print, localStorage always-auto-print opt-in); industry-standard retail anatomy (v0.7.253, letterhead + GST/PST# from shop_config, SKU + qty @ unit, Tendered / Change); "Served by <initials>" from __actor.name (v0.7.255); unified detail-panel Print onto the same receiptHtml with a docLabel switch — sale / refund / estimate / layaway / SO / invoice (v0.7.256); Estimate prints as a full-page Letter quote with terms + validity (v0.7.257, buildEstimateFromCart); print-prompt on estimate create (v0.7.258); humanized tax field name (v0.7.259). Deposits render a balance-owing line (v0.7.296). Issued gift-card number on the post-sale prompt header (v0.7.326).

  • Core keyboard layer. New Kernel primitive core/keys.ts (v0.7.282). LIFO Escape stack, shortcut registry, per-tab onEscClear, "?" cheat-sheet. Every modal / popup pushes onto the stack; Escape dismisses the topmost one at a time. New Bible page: Core keyboard layer.

  • Tier-0 / 1 / 2 shop-owner audit sweep (Sales money-bug backlog closed):

    • Tier-0 #2 — stray-click on payment no longer takes money (v0.7.315). 42-px gap between Split and the next tender tile, an "or" divider, confirm-step on every tile.
    • Tier-0 #3 — cancelled sales no longer vanish (v0.7.317). Cancelled sales stay inline in Today's Sales, struck-through, no hidden footer (v0.7.319).
    • Tier-1 #5 — On-account label + Receivables view (v0.7.324/.325). On-account sales read "On account", not "Invoiced"; own status-filter entry, amber pill. Customers-tab Receivables view (GET /receivables, red owed pill on header, #recv route, mutually supersedes YoY).
    • Tier-1 #6 — Cancel & refund honours original tender (v0.7.322). deposit-cancel refund goes back to the tender the deposit came in on; ledger row shows the tender name.
    • Tier-1 #7 — GC / Account-credit lines tax_category null (v0.7.323). addLine hardens against the (already-fixed) case where a gift card or account credit was carrying a taxable category.
    • Tier-1 #8 — Gift-card number on the post-sale prompt (v0.7.326). Slip printed with the actual card number.
    • Tier-2 #9 — Off-day rows dated in the single-date view (v0.7.327). Parked-across-days rows show "Jul 12, 9:43 p.m." not just a time.
  • Sales ledger refinement. Special-order lifecycle broken into Awaiting order → Ordered → Order Arrived (gold, #fde68a) → Ready for pickup (v0.7.268/.270/.273/.274); after arrival becomes payment-aware (v0.7.248). Date-range picker works inside a status view (v0.7.277). Default status chip reads "Today's Sales" (v0.7.281). Today's Sales total is NET of refunds (v0.7.289). Local-timezone ledger dates (v0.7.279).

  • Refund + tender defaults. defaultRefundMethod() (v0.7.291) picks the destination from the original payment (card → card, cash → cash, on-account/store-credit/gift-card → store-credit). Red warning on cash-vs-non-cash mismatch; amber on other mismatches; hidden on match. Estimate button gated to customer-attached (v0.7.280).

  • Card brand + last-4 everywhere. payLabel() (v0.7.292) — brand replaces the generic "card" label; ····1234 appends when present. Hand-keyed card capture (v0.7.293) — type + last-4 + auth, all optional, rides card_brand / card_last_four / reference_number. Nothing on the shop side stores a PAN.

  • Editable tax on active transactions (v0.7.267). POST /set-transaction-tax re-prices stored lines, rebuilds transaction_taxes, updates totals, re-derives deposit balance.

  • Customer ledger mirrors Sales. Summary column (v0.7.285), Status pills with STATUS_PILL_PALETTE (v0.7.286), shared row-builder (v0.7.288), Cashier column in single-customer view (v0.7.262). Held / Pending status text unified (v0.7.287 rename → v0.7.294 revert to Pending).

  • Product-click → Inventory editor, in place (v0.7.260/.261). kbEnsureModule('inventory') mounts Inventory hidden if unvisited; Inventory reparents its New-product modal to <body> on mount and exposes window.kbOpenProductEditor(vid). Sales fetches variant + opens same editor as overlay. Reused, not copied. v0.7.261 fixes the first-click ensure-mount guard.

  • Reassign customer on posted transaction (v0.7.266). Detail-panel customer header opens the Attach popup; POST /reassign-customer audits, blocked when on-account or store-credit rows tie the sale to the original customer.

  • Take-Payment banner (v0.7.301). Before money moves the modal names "Special Order" or "Layaway" if any line is out-of-stock or on order; receipt flags on-order items. product_type gate filters services + rentals out of the special-order trigger.

  • Tab-accent picker (migration 042, v0.7.263 → .265). Owner-only shop-wide tab colour picker in Settings — rainbow / pastel / colour-wheel, live preview, apply-on-pick. shop_config.tab_accents_json; every --accent reads through the same helper.

  • Till chrome + chrome. Idle-Till New Sale button (v0.7.276). Header reads "Current Sale — <Name>" / "Current Sale — (Empty)" (v0.7.278). Clear × is a bold serif "C" (v0.7.312, DM Serif Display 700). Layaway / SO buttons removed from every status (v0.7.250). Fold account management into Edit-customer modal (v0.7.311). Site-wide autofill disabled via core/noautofill.ts (v0.7.283). Nav-tab underline retired (v0.7.284). Till column resizer un-bundled + bike-silhouette on the accent rule (v0.7.244–.246, .239). Inventory-adjusted toast colour-keyed to Inventory tab (v0.7.254). "Actual Stock Count" renames OOS stepper label (v0.7.272).

  • Audit narrator house-style (v0.7.269). narrateTerse() + a full idiom map in core/audit-narrate.ts, cents → $X.XX, arrows and × normalized; ad-hoc ACTION_PHRASE retired.

  • Version-notes popup + generated changelog (v0.7.291, v0.7.316, v0.7.321). New Kernel modules core/versionmodal.ts + core/changelog.ts — auto-opens once per build via localStorage, ESC dismisses via kbKeys, generator renders the full commit body per entry. Refresh-the-card commits (no version bump) are dropped from the modal.

  • New migrations. 041_payment_reverse_perm.sql, 042_tab_accents.sql, 043_tooltip_mode.sql, 044_feature_registry.sql (129 seeded hints), 045_pos_tender_flags.sql, 046_special_line_on_line.sql.

v0.7.239 — Till + Ledger + Charge popup at prior-implementation parity (2026-07)​

The 132-patch stretch that took Sales from "cart + tender" to "one Charge popup, in-Till detail panel, backorder-rings-through, audit narrator + ticker" — the substrate the operator now actually lives in. Grouped by cluster, patch counts approximate.

  • Audit narrator + ticker footer (v0.7.182 → v0.7.200). New Kernel modules: core/audit-narrate.ts (deterministic verb/lexicon library keyed by event_type) + core/audit.ts gained recordAuditEvent (FULL-coverage tamper hash) + recordNarrated + recentAuditEvents reader; migration 040 audit_events; /api/audit/recent. The shell footer becomes a 25-px ticker tape below every tab (v0.7.195); toasts retired (v0.7.175); verbose hover peek only, click-to-open removed (v0.7.200) because a glanceable ticker isn't an inbox. Sale / void / refund / deposit-create / add-funds / deposit-cancel all route through recordNarrated. Even terse writeAudit entries are now owner-readable — the narrator resolves variant #N / staff #N / transaction #N into readable names + actor + verb. New Bible page: Audit narrator + ticker.

  • Backorder → Bucket flow (v0.7.190 → v0.7.204). The old "can't add — out of stock" popup is gone. Zero-stock lines ring through — red backorder N on the line; on Charge, the shortfall auto-drops into inventory_order_queue (the Purchases Bucket), earmarked to the customer if attached (blue chip) or for Stock (neutral) if not. Sales posts as a Special Order; the ledger row tracks Awaiting order → Ordered → Partially received → Ready for pickup downstream. OOS pill reframed as "Adjust actual stock" — an inline kbStepper that commits live to inventory.quantity_on_hand via /count with an "Inventory Adjusted" popup, for the miscount-correction case. Manual /reorder endpoint retired. New Bible page: Backorder → Bucket flow.

  • Charge popup unification (v0.7.208 → v0.7.214). ONE openConvertPay/Charge popup drives active-Till AND detail-panel commits. Footer is Cancel · Estimate · Charge; the transaction type auto-derives from what the operator enters — editable Total (partial → open order or layaway; full → sale; zero → estimate or "place the order — no payment" for $0 special orders, BIKE.L2-0212). Paid-off deposits hide Charge / Layaway (v0.7.208). Take-payment Total is obviously editable (v0.7.214) — solid border, radius, subtle fill, hint.

  • Take-payment modal at prior-implementation parity (v0.7.206). 3×3 grid of nine tender tiles with emoji + one-line how; Debit / Mastercard / Visa become colour-keyed brand variants of card_present (blue / red / indigo); big DM Serif TOTAL DUE; shared tenderGridHtml drives every tender surface (sale, deposit, refund-out, corrected-pay) so a change lands everywhere.

  • Sales ledger overhaul (v0.7.204 / .205 / .215 / .216 / .218 / .220–.223 / .234–.239). Priciest line + "+N" on multi-item rows (N = physical items, SUM qty). Status filter dropdown from the Status column header; Cancelled hidden by default. "All <Status>" cross-date view + infinite scroll — /today gains status + before_at/before_id keyset pagination. Sticky column header — thead moves out of the scroll region. "All <Status>" heading kept when clearing till from a status view. Customer name on a historic transaction opens Edit Customer, not re-attach. kbDatePicker.restoreLabel preserves date/range across open + close. Owing amount under Total; red R flag as pill-corner badge for returns. Hover cards — monochrome, unified, cross-referenced (/transaction gains related_refunds + refund_of so an invoice card lists its refunds and a refund card lists its invoice). Till column resizer un-bundled (v0.7.239).

  • Return / Refund maturation (v0.7.225 → v0.7.232). Return button drops off when nothing's returnable; red R flag + stacking return hover card from /sale-returns (staff + per-refund lines + fully_returned). Standard kbStepper on qty; refund estimate includes tax. Return modal keeps the invoice up behind it. Refund returns the actual tax paid (effTax = total − subtotal + discount), proportions to the header not the line prices (imported sales don't reconcile at line level), and for imports with no tax rows infers GST 5% + PST remainder. Return hover card shows both invoice and refund side.

  • Layaway / Special-Order gating (v0.7.192, .203, .208). On a walk-in, Layaway / Special Order / Estimate open the attach-customer popup with Keep walk-in hidden; pendingAction auto-fires the action once a customer's picked or created. /deposit and /estimate server-guard missing customer with 400. Paid-off deposit hides Charge + Layaway (v0.7.208).

  • Purchases Bucket owner display (v0.7.191). Every Bucket row now shows "Awaiting order · for Stock" (neutral) or "for <Customer>" (blue). /bucket returns customer_id + special_order_transaction_id + customers.display_name. Vocabulary: Bucket = "Awaiting order", PO cut = "Ordered".

  • New Kernel primitives. core/stepper.ts — one shared −/+ control mounted on Sales cart, Purchases Bucket, new-PO staging, Return panel qty (BIKE.L1-0013). core/customeredit.ts — shared Edit-customer control (v0.7.185). core/router.ts gained kbGoto(tab) cross-module opener (v0.7.120). core/formats.ts data-mask="money" primitive (v0.7.210) — type=text inputmode=decimal, shows 5,999.99 on blur, strips commas on focus so no read-side ever undercounts. Phone-mask primitive alongside (BIKE.L1-0002).

  • Till chassis unification (v0.7.108 → v0.7.189, ~50 patches). The substrate on which every cluster above lands: 3-zone sales-3zone split (v0.7.128); transaction detail on the Till chassis — opening a historic transaction swaps the same slot into a detail panel, not a modal (v0.7.130, BIKE.L2-0208); Customer-Till look emulation (v0.7.113); search-bar as sole entry (v0.7.142); phase-aware search (v0.7.145) — the box's behaviour shifts by Till phase; autopark on tab-away / 90s idle (v0.7.152 / .157 / .164, BIKE.L2-0220); Cancel/Park doctrine (v0.7.146 / .149 / .151); Cancel + void revert ledger to Today (v0.7.186 / .187); editable detail lines for non-finalised transactions (v0.7.181); negative-qty till → auto-refund (v0.7.139, BIKE.L2-0215); live multi-user poll (v0.7.140, BIKE.L1-0017); tab-return in-till refresh (v0.7.150, BIKE.L2-0216); bulk items as real SKUs via is_bulk (v0.7.136 + migration 039, BIKE.L2-0214); "Awaiting order" ledger status (v0.7.132); Till header default flipped from "Customer" to "Walk-in" (v0.7.189); one-Till-layer + customer-history ledger (v0.7.164); tab-accent color-key on the Till header primary (v0.7.162).

  • New migrations. 035_shop_config.sql (BIKE.L1-0012, v0.7.108). 036_layaway_backbone.sql (BIKE.L1-0014, BIKE.L2-0209, BIKE.L1-0015, v0.7.115). 037_special_order_backbone.sql (BIKE.L2-0210 / 0211, v0.7.116). 038_aim_full_fidelity.sql. 039_variant_bulk_flag.sql (BIKE.L2-0214, v0.7.136). 040_audit_events.sql (v0.7.182).

  • Small QoL. Customer card Account balance is signed money — -$X owing / $X credit (v0.7.213). .cd-drow .nm unified across Invoiced + Awaiting-Order detail views (v0.7.207).

  • v0.7.107 — Color-coded Sales ledger status pills (BIKE.L2-0163). The flat .sc-chip status cell in the ledger becomes a rounded status→color pill from a 9-pastel palette copied verbatim from the prior implementation. Invoiced=mint, Cancelled=red, Layaway=cyan, Refund=pink, Estimate=purple, Special order=peach, Parked=sky-blue; auto-parked rows carry an inverted AUTO sub-badge. Unknown statuses fall back to mint, never an unstyled chip. .sc-chip still holds the cart / detail / inventory / purchases uses; sorting (L2-0205) + the row hover-preview (L2-0204) untouched.

  • v0.7.106 — Click-the-tax-line "Adjust tax" modal (BIKE.L2-0067). The GST and PST rows in the till totals are now clickable — opens a 3-mode Adjust Tax modal at prior-implementation parity: Status Card (PST-exempt via card number), Manual override (enter cents on either tax, reason required, client + server validate), or Default (revert to the computed per-line total). Modes are mutually exclusive; an override badge lands in the totals block while active; Park / Clear / new-sale wipes it. Sits on top of the per-line categories — the override replaces the computed GST / PST total.

  • v0.7.105 — AIM employees mirrored into staff; Tom relocates to id 999 (auth resolves by role). New template migration 034: staff.external_aim_id + start_date + username (+ index). Client data: data/staff_roster_swicked.sql mirrors the 9 AIM employees (id 1–9: Jordan Fair, James Durand (owner), Rob Pengelly, Les Jank, Chenoa Campbell, Liam Dyck, Khoen Montgomery, Robbie van Boot, Jadyn Steinhauer) into kvick-bike at the SAME ids the imported transactions already reference — no FK rewrite. Tom's bootstrap admin relocates from id 1 → 999 so Jordan Fair (12,457 imported sales) owns id 1 cleanly; core/auth.ts now resolves the sys-admin bootstrap by role, not by id=1.

  • v0.7.104 — Sales ledger row hover-preview card (BIKE.L2-0204). New Kernel control src/core/hovercard.ts — a shared row hover-preview shell with per-row arm delay, fetch-or-local data hook, viewport-aware positioning at document.body level (escapes ledger card overflow:hidden), race guard on late fetches. Sales mounts it first: posted rows fetch /api/m/sales/transaction?id= at 400ms and cache; parked rows render at 350ms from local till data. Card mirrors the prior implementation exactly — voided note, SPECIAL ORDER band + PO status, ref+type header, Rung-by, Customer+acct, scrollable line rows, tax rows, Total. Service / PO / Inventory / Customers reuse the same shell later.

  • v0.7.103 — Sales attach-customer popup: prior-implementation parity with inline create (BIKE.L2-0002). Rebuilt #sc-cust-modal to duplicate the prior picker: live search-as-you-type over name / phone / email / account #, enriched result rows, subtitle hint, walk-in / cancel / swap paths. The main gap filled: an inline + New customer create panel (Name required + Phone / Email side-by-side + retail-default helper note) that POSTs /api/m/customers/create and attaches the new customer in one step. Empty / no-match results double as a "+ New customer" affordance; Enter picks the first match; name prefills the create form from the search text.

  • v0.7.102 — Removed the customer spend graph from the Sales till (BIKE.L2-0156). The spend-over-time strip duplicated the identical graph on the Customers card, and re-fetched /spend-history on every attach. Deleted #sc-spend-strip + loadCustomerSpend() and its four call sites from Sales. GET /api/m/customers/spend-history + the Customers-card graph (#cd-spend-graph, Monthly/Weekly/Annual toggle) untouched.

  • v0.7.101 — Sales ledger: date in the Time column when not today + year grouping in customer-history mode. The first column shows the DATE for any row that isn't from today, time only for today's rows — a customer's multi-day history reads by day. Customer-history mode groups rows under year section headers, newest year first.

  • v0.7.100 — Sales ledger flips to the attached customer's sales history (BIKE.L2-0025, prior-implementation parity). Attaching a customer flips "Today's Sales" into customer-history mode — heading reads "<Name> — sales history", the ledger shows that customer's whole sales history (newest first) plus their own parked sales; the date picker is disabled while attached. Walk-in / Clear / sale completion restore Today's Sales. Resuming a parked/estimate sale deliberately does NOT flip. Backend: GET /today gains customer_id (wins over the date window, limit 200).

  • v0.7.99 — Consistent ledger headings: title case, no ALL-CAPS. Sales + Service had inherited .section-header uppercase + letter-spacing, so "TODAY'S SALES" / "TODAY'S SERVICE ORDERS" shouted while Customers / Inventory read normally. Normalized all five headings to title case: Today's Sales · Today's Service Orders · Today's Visits · Stock Movements · Purchase Orders.

  • v0.7.98 / v0.7.97 — Universal ledger date navigator on Sales, Service, Inventory (BIKE.L1-0010). Mounts the shared kbDatePicker on every module's ledger heading (Customers already had it). Service (/ledger) and Inventory (/movements) endpoints take an optional date_from/date_to window; both ledgers are work/history feeds so today (the default) shows the FULL board (current work-queue / whole movement history) and only a picked past day or range applies a filter. Sales' /today takes the same window; parked sales only show on the current day.

  • v0.7.96 — sales-3zone column ratios match updated BIKE.L1-0001. The responsive spec differentiates the two upper breakpoints: tablet 40% / 60%, desktop even 50 / 50 (was single 60/40). Substrate Line canonical split is now 50/50. Verified at all three viewports.

  • v0.7.95 — Sales till status flows inline after the title (prior-implementation parity). The Till open · cashier <name> · N sales today · $X status line was being pushed to the far right of the .page-header. Wrapped title + sub in a baseline flex group so status flows inline right after "Sales" on the left.

  • v0.7.94 — Sales till header shows who is signed in (Sales-only). The Sales page-sub now reads "Till open · cashier <name> · N sales today · $X". GET /api/m/sales/summary returns ctx.actor.name/role; the Sales client renders the cashier segment into #sales-sub. Sales-only; other module headers untouched (no clickable switch-cashier pill yet).

  • v0.7.93 — Color-keyed accent line under every module header (Foundation). Each module's .page-header gains a 2px bottom rule in that tab's accent colour (--accent, set per-tab on <body> by the shell + router). One Foundation CSS rule covers every module; Settings was the only tab not using .page-header, restructured into the standard .page-title/.page-sub so it inherits. Also v0.7.93 — Family matches got their own visible group in Sales search (later reverted, superseded by v0.7.92 rerank+cap fix).

  • v0.7.92 — Sales search actually surfaces family-member matches. Migration 031 already added family_names to search_customers_fts; the trim was hiding legitimate matches. Two fixes: rerank() gives family-name prefix matches a +300 tier (real signal, still below the account's own name), and Sales' limit went 8 → 20 so a legitimate 14th-place match isn't silently cut off. Family match rows show Family: alex, owen, aaron, … so staff see why she matched.

  • v0.7.87 → v0.7.91 — Focus-stewardship doctrine across all five module tabs (BIKE.L2-0195/0196). New Kernel control src/core/focus-steward.ts — a MutationObserver-driven registry that reclaims focus for the active module's steward input after a modal closes, a print dismisses, focus blurs to nothing, or 5 seconds of keyboard idle. Capture-phase scanner-burst interception (≥5 chars, ≤50ms between keys, ending in Enter) redirects into the steward regardless of where focus drifted. Adopted by Sales (v0.7.87 #sales-search), Service (v0.7.88 #sv-search; also completes the port with the "don't yank from another visible input" guard + 5s idle timer), Customers (v0.7.89 #customers-search), Purchases (v0.7.90 #pur-search), Inventory (v0.7.91 #inv-search). Sales also picks up Enter-opens-first-result / Escape-clears-and-closes / multi-add flash-green-keep-open / cash-tender auto-select / Enter-confirms / Escape-cancels / $5/$10/$20/Exact quick-tender presets (BIKE.L2-0197–0201). New Bible page: Focus stewardship.

  • v0.7.86 — Special orders auto-post to the PO Bucket; ledger tracks it live (James S13). A Sales special-order deposit auto-posts a line into the Purchases Cart the moment it's taken; migration 033 links Sales transaction → Bucket line → PO line so the Sales ledger row can render live status: Special order → Ordered → Partially received → Ready for pickup.

  • v0.7.84 — Deposit / balance owing in the live till footer (James S19). The tender modal used to show it, but not the cart footer; now the cart footer surfaces deposit applied + balance owing while the operator is still stacking lines.

  • v0.7.83 — Cancel & refund a layaway / special-order from the ledger (James S11). Row action on a deposit_layaway / deposit_special_order transaction. Refunds the paid amount + releases the reserved stock (special orders) + optional restock (layaway).

  • v0.7.82 — Reverse a payment entered with the wrong amount or method (James S9). From transaction detail: reverse a specific transaction_payments row (creates a mirror-signed entry + audit) so the next payment lands with the corrected method / amount.

  • v0.7.81 — Accessories bundled with a bike drop PST; standalone both taxes (James S5). Migration 032 seeds the is_context_dependent + context_rule on tax_categories; Sales computePerLineTaxes reads the context at cart time (BC PST Bulletin 204). Ported verbatim from the prior implementation.

  • v0.7.80 — Family members are searchable; restocking fee can be a percent (James). Migration 031 adds family_names to search_customers_fts with triggers on both sides. Restocking fee gains a percent-of-return option next to the fixed-dollar option.

  • v0.7.79 — Merge Return and Exchange into one Return, pick destination at completion (James S11). The Return panel now runs the same flow regardless of destination; at Complete, the operator picks refund method (cash / card / store credit / on-account). Removes the "did I click Return or Exchange" decision at the start.

  • v0.7.78 — Resume an open estimate / layaway into the till for editing (James S18). Row click on an open estimate or layaway loads its lines back into an empty cart; edits ride the same estimate / deposit save path (not a fresh sale).

  • v0.7.77 — Layaway / special-order to sale, using deposit credit (James S15). Pickup / completion converts the deposit transaction to a sale, applies the deposit as a payment, and collects only the balance.

  • v0.7.76 — Estimate-to-sale: convert in place with a real charge (James S14 + S16). The Convert button on an estimate opens the tender modal with the estimate's lines; posting rings the sale + closes the estimate in one operation.

  • v0.7.75 — Remove Void as a one-click action on completed invoices (James S6). Void needs a reason + sales.void gate; the ledger row's one-click was too easy to hit accidentally. Void now lives inside the transaction detail modal.

  • v0.7.74 — Sales ledger real status labels (James S13). Special order → Ordered → Partially received → Ready for pickup etc., driven by the live substrate rather than a static per-type string. Feeds off migration 033.

  • v0.7.72 / v0.7.73 — Tax inheritance chain (James S3) + perm-gate regression fix. Per-line tax category falls back through variant override → product default → category default → tax_categories default so the operator never sees a line without a tax category resolved.

  • v0.7.71 — Sales: clear search bar on any selection (James S2). Attaching a customer, adding a line, or picking a bulk-item clears the search box so the operator can immediately type the next thing.

  • v0.7.70 — Purchases: create a SKU from the Cart (Omega #49 fix, BIKE.L2-0049). The Cart's Add-a-product search gains a + New SKU button that opens the create-product modal; the new SKU is then searchable + addable to the Cart. Closes the same gap the PO-line editor path already had.

  • v0.7.69 — Products split into two modules: Inventory + Purchases. The former two-face module becomes two peers: src/modules/inventory/ (SKU search → variant detail → Product Card editor → movement ledger) and src/modules/purchases/ (PO ledger + Purchase Cart + Vendors + PO lifecycle). The movement engine is extracted to core/movements.ts so Sales and Service can decrement stock through the same code path.

  • v0.7.68 — Inventory: remove orphaned section-edit chain (Product Card supersedes).

  • v0.7.67 — Products Face A (Inventory) rebuilt to prior-implementation Inventory-tab parity.

  • v0.7.66 — Products Face B (Purchasing) rebuilt to prior-implementation POs-tab parity.

  • v0.7.65 — Sales: split-tender (BIKE.L2-0058). A sale can be paid with several tender rows in one transaction — cash + card, deposit + cash, etc. — with per-row change handling.

  • v0.7.64 — Foundation: field & format primitives — core/formats.ts (kbFmt) for money-cents / ISO dates / percent / phone formatting, used across every module's read + edit views.

  • v0.7.63 — Clear button on Sales + Service tills. Empty the active cart / work-order with kbConfirm before losing state.

  • v0.7.62 — Service module (BIKE.L2 service tickets). Workshop work-order screen — search + today's-service-orders ledger grouped by status; drop-off ticket → labour (service_menu) + parts (variants) → status lifecycle (Start Work / Wait for Parts / Stop·Call / Ready / Notify) → pickup invoices as service_invoice sale. Migration 030. See the Service module page.

  • v0.7.61 — Sales: customer spend strip in the till (BIKE.L2-0156). When a customer is attached to a sale, a compact monthly-spend bar strip + this-year total / YoY% renders below the till header (reusing /spend-history) — see the spend while ringing the sale, where the Omega looks. The full chart + toggle stay on the Customers card.

  • v0.7.60 — Omega fixes. Estimate Convert-to-sale button (BIKE.L2-0071; was read-only). Reorder dashboard always reachable for reorder-capable operators (BIKE.L2-0089; was hidden at 0 low-stock). Library reconciliations: un-marked 0141 (segments never built), corrected 0156 location (Customers tab, not Sales till), 0095 / 0140 narratives (click-to-open modals, the doctrine).

  • v0.7.59 — Products: Print tag (BIKE.L2-0047). Stock Card footer Print tag → preview (product name, price, hand-rolled CODE39 barcode of the SKU, SKU text) → Print window. Single loaded SKU; batch-from-ledger + templates reconciled to a follow-up.

  • v0.7.58 — Customers: spend-over-time graph (BIKE.L2-0156, Sales-unblocked). /spend-history returns per-period total posted-sale spend + fiscal-YTD compare; Spend section renders a bar chart (Monthly / Weekly / Annual toggle, YTD-bordered bars, per-bar tooltip) replacing the placeholder. Category stacking deferred.

  • v0.7.57 — Products: + New SKU from the PO line editor (BIKE.L2-0049). Add-a-product search in the PO line popup gains a + New SKU button that opens the create-product modal; the new SKU is then searchable + addable to the PO.

  • v0.7.56 — Products: 7-view analytical ledger (BIKE.L2-0149 + 0145 top sellers). /movements?view=X serves movement-shaped views (today / sold_today / last_received) and variant-shaped views (top_sellers / bottom_sellers / out_of_stock / old_stock); the Ledger Pill (Tier 2) selects the view, the ledger <thead> + rows adapt per shape, variant rows click through to the Stock Card. Replaces the stopgap movement-type filter.

  • v0.7.55 — Products: Recent purchasers + click-to-sale (BIKE.L2-0148, Sales-unblocked). /variant now returns recent_purchasers (who bought this SKU, newest first w/ sale_id); the stock-card section renders the rows; a click opens a read-only sale viewer.

  • v0.7.54 — Sales: enforce discount.apply (was a defined-but-unwired orphan perm). /create 403s a discount when the operator lacks discount.apply; client hides the discount chips + cart-discount Apply and makes the line price read-only. Completes the Sales permission surface (every sales.* + discount.* gate now enforced).

  • v0.7.53 — Re-baseline from v0.8.6: revisions one decimal finer. Keep the 0.7 minor + patch-per-completion (incl. new modules), monolith-0.6.470-style (low minor / high patch); minor + major reserved, 1.0.0 = parity + cutover. The v0.8.0–v0.8.6 Sales bumps map onto the patch line continuing v0.7.46.

The v0.8.x Sales bumps below were folded into the v0.7.x patch line by v0.7.53 — kept as record:

  • v0.8.6 → folded — Sales: transaction-level discount (BIKE.L2-0060) + per-line tax override (BIKE.L2-0068). Cart-target customer discount as a totals chip (Apply / remove), reduces subtotal and GST/PST base proportionally; per-line tax-category picker via /tax-categories; taxApplies() uses applies_gst / applies_pst. Sales 21/28 spec-complete.

  • v0.8.5 → folded — Sales: store-credit exchange + return panel (BIKE.L2-0009, full HUB spec). Return / Exchange panel — per-line qty + condition (New / Used restock; Damaged does not), restocking fee, header reason. Exchange issues store credit to the original customer (migration 029) and opens a fresh sale with that customer attached; the pay modal pre-applies store credit and collects only the difference.

  • v0.8.4 → folded — Sales: line-discount rule engine (BIKE.L2-0059, full HUB spec). Customer-assigned model: /discount-suggest resolves the attached customer sales discount + action; per-line chips (percent / dollar / set_price); manager-threshold gate via discount.override_manager (client kbCan + server-authoritative in /create); applied discounts recorded to transaction_discounts with transaction_line_id + discount_id (migration 028) and total_discount_cents on the header.

  • v0.8.3 → folded — Sales: the 4 remaining buildable L2 — void (BIKE.L2-0063: reverse stock + ledger, mark voided, sales.void), line discount (BIKE.L2-0059: editable cart unit price + original_unit_price recorded, discount chip), exchange (BIKE.L2-0009: return remaining lines + start fresh sale), print receipt (BIKE.L2-0069: print window from the detail modal). 19/27 Sales L2.

  • v0.8.2 → folded — Sales: allow overselling on a sale (movement engine allowNegative flag) so a POS sale of a zero / backorder SKU posts, like the prior implementation. /create + /from-estimate set it.

  • v0.8.1 → folded — Sales module (slice 2): the transactional write path. /create (post sale + lines + multi-tender + GST/PST + status-card exemption + stock decrement + TRX-YYYY-NNNNNN), /refund (per-line restock), /transaction detail, /estimate (create / void / resume / from-estimate), /deposit (layaway + special-order + add-payment + cancel), /parked CRUD. Client: tender flow (7 methods + cash change), estimate, layaway / special-order deposit, park / resume, ledger → receipt → return.

  • v0.8.0 → folded — Sales module (slice 1): scaffold + register + schema (migration 027) + read surface. sales-3zone .till, today's-sales ledger, unified search (product / customer / invoice), client-side cart with live GST/PST preview, bulk-items palette, attach-customer modal. See the Sales module page.

  • v0.7.46 — Products / Purchasing: transcribe the prior system's POs-tab Bucket into Face B (Pill UI kept) — search-to-add-to-cart, inline bucket line controls (qty stepper / cost / vendor), bucket cost-override (migration 026).

  • v0.7.45 — HOTFIX. The Products tab had been dead since v0.7.40: an escaped apostrophe in the client template emitted a bare quote → SyntaxError killed the whole module script. Removed it ("Cost cannot be negative").

  • v0.7.44 — Products / Purchasing: receiving depth — cost-override + cost ladder on receive, supplier_products cost stamp, default-0 / over-receipt, discrepancy roll-up (BIKE.L2-0044 / 0083).

  • v0.7.43 — Products / Purchasing: Clear bucket — empty all staged lines across vendors (BIKE.L2-0050).

  • v0.7.42 — Products / Purchasing: New PO from scratch (vendor picker → empty PO → line editor) + PO ledger column sort + vendor filter (BIKE.L2 New-PO / 0041 / 0048).

  • v0.7.41 — Products / Purchasing: rich vendor record — account #, address, website + B2B portal, logo, drop-ship in the supplier popup (BIKE.L2-0051).

  • v0.7.40 — Products / Purchasing: editable Order Card — edit pending PO lines (qty / unit-cost), add + remove products, server-side total recompute (BIKE.L2-0042).

  • v0.7.39 — Products: align PO status lifecycle to the prior system (pending_order / ordered; migration 025).

  • v0.7.38 — Fix: .module-pane display:contents to restore the module's flex layout under the keep-mounted router.

  • v0.7.37 — Foundation: keep-mounted SPA tab navigation (BIKE.L1-0008). Modules mount once and persist across tab switches. New src/core/router.ts. See Keep-mounted SPA navigation.

  • v0.7.36 — Products: Vendor RMA operator UI (BIKE.L2-0091 complete).

  • v0.7.35 — Products: Vendor RMA engine — tables + lifecycle backend (BIKE.L2-0091; migration 024).

  • v0.7.34 — Products: wire the reorder dashboard to inventory.reorder.

  • v0.7.33 — Products: reorder dashboard — low-stock surfacing + PO from thresholds (BIKE.L2-0088 / 0089).

  • v0.7.32 — Products: gate the Inventory / Purchasing switch on purchasing.view.

  • v0.7.31 — Settings: consolidate Products permissions into one matrix area.

  • v0.7.30 — Products: bulk price update (BIKE.L2-0087) — multi-select variants and apply a percentage or fixed change in one popup; one audit row per variant.

  • v0.7.29 — Products: supplier editing from the Order Card.

  • v0.7.28 — Products: clone a product (BIKE.L2-0045).

  • v0.7.27 — Settings: gate the staff-screens override endpoints (authz fix).

  • v0.7.22–v0.7.26 — Products: create-from-scratch flow, conform buttons / pills / modals to the Customers norm and the shared .kb-modal family.

  • v0.7.21 — App-wide timezone fix: render timestamps in the shop's tz.

  • v0.7.16–v0.7.20 — Purchasing (Face B) read surface → PO Bucket (add-to-PO grouped by supplier) → build & issue a PO → receive (closes the Bucket → PO → on-hand loop).

  • v0.7.12–v0.7.15 — Inventory-movement engine + Adjust (BIKE.L2-0046) + Cycle count (BIKE.L2-0084) + Write-off (BIKE.L2-0085); filtered ledger views (movement-type chips).

  • v0.7.10–v0.7.11 — Products section-edit popups (process 0147): Pricing & reorder + Tax + Product + Variant.

  • v0.7.4 — Imported the real catalog from the prior system.

  • v0.7.1–v0.7.3 — Products foundation: data model (catalog + inventory ledger + purchasing; migrations 018–020) + two-face module scaffold + the sales-3zone 60/40 layout that every tab inherits.

  • v0.7.0 — Re-baseline. The in-app Bible link was removed at the same time (the BIBLE_URL was a placeholder; the Bible stays a separate deployable at kvick-bike-bible.pages.dev).

See the Products module page for the worked architecture.

History — v1.x (kept as record; folded into v0.7.0 baseline)​

v1.4.2 — Customers Till header is name-first​

The customer Till header leads with name + phone/email (PII-gated), dropping the redundant "Customer" label and moving the account number into the Identity section. Empty state reads "No customer attached".

v1.4.1 — Family members refinement (refines BIKE.L2-0157)​

Each in-card family-member row is click-to-edit — the popup pre-fills, Save updates via PUT /family-members, Remove moved into the popup. The Relationship field was dropped (the column stays, written NULL); the member's phone now shows inline in the slot the relationship used to hold (Name · Phone · minor).

v1.4.0 — Year-over-year header (BIKE.L2-0158)​

The Customers tab now leads with a churn-vs-growth pulse: total active · ▲ spending more · ▼ spending less. New customers_yoy_summary precompute (migration 017) refreshed by a nightly cron (03:17 Vancouver). See Customers module for the full surface.

v1.3.1 — Spend section placeholder (BIKE.L2-0156, partial)​

Added a Spend slot in the customer card; the real stacked-bar graph + year-over-year numerals are deferred — they need per-line product categories from Sell / Fix's domain.

v1.3.0 — Editable Permissions Inspector (BIKE.L2-0155)​

The role × permission matrix is now fully editable for sys.permissions.edit holders. Custom roles + per-staff overrides (override role + per-permission grant / revoke / inherit) + Reset Owner permissions for sys-admin recovery. Migration 016 added staff.override_role_id and granted Owner sys.permissions.edit. See Settings module → Editable Permissions Inspector.

v1.2.1 — Phone frame scroll fix (BIKE.L1-0001)​

The sales-3zone phone layout was viewport-locked. Released the app-shell lock on phone — the window scrolls, the .till is content-height, each pane is a sized scroll box.

v1.2.0 — Foundation responsive layer (BIKE.L1-0001)​

The chassis-level responsive primitives that every module inherits.

v1.1.1 — 2026-06-25​

  • Removed the dead .helm-editable inline-edit CSS (the contenteditable chassis was retired pre-v0.6.385; no element used the leftover stylesheet) and corrected the stale "inline-editable card" comment. The customer card has no inline-edit affordance — editing is modal-first per the no-inline-editing doctrine. Reconciles the build with the BIKE.L2-0023 process narrative (modal-first edit; pricing_tier dormant; start-fresh, no A/B/C→Trade migration).

v1.1.0 — 2026-06-25​

Customers​

  • Account linking renamed "Family" → "Linked" — the section links separate customer accounts (personal ↔ business, partners who each keep their own account), not family members. API /family → /links; audit customer.account.link / unlink; the customer_family_links table is kept as an internal legacy name. (Edits process BIKE.L2-0153.)
  • New "Family" section — in-card family members: a primary contact plus spouse / children, each with their own name, relationship, and phone (+ minor flag). New customer_family_members table; /family-members CRUD (migration 015). A different level from account links — members of one account, not other accounts.
  • Card layout is now Contact → Linked → Family → Marketing → …

v1.0.0 — 2026-06-25​

Baseline of the clean-room rebuild (started 2026-06-21), replacing the prior ~90k-line implementation. Everything below ships in 1.0.0; semantic versioning begins here.

Core (L1)​

  • Tiny kernel: D1 wrapper, DB-session auth + actor, hash-chained audit log, module contract + registry, granular permission seam (can() over a data catalog).
  • Cloudflare Workers + D1, strict TypeScript, no framework; HTML / CSS / JS inline in TS template strings.
  • Shared Core controls injected once in the shell: kbDatePicker, kbSortable, kbPager, kbPrint, and the FTS5 search engine (core/search).
  • cache-control: no-store on shell + API responses (no stale bundles after a deploy).

Customers (L2)​

  • Full customer card at parity with the prior build: 3-pane work area (search / swappable ledger / read-only Till), modal-first editing per the no-inline norm.
  • FTS5 search (BM25 + per-token prefix + synonyms); keyset-paginated interactions ledger (no silent caps).
  • Sections: Contact / Linked / Family (bidirectional links + minor flag) / Marketing / Notes / Equipment / Loyalty / Identity / account credit-debit / Merge / Print.
  • Real dataset imported from the prior build: 3,099 customers · 2,541 bikes · 20,360 transactions.

Settings (L2)​

  • Staff & permissions: Employee modal (role, visible tabs, PIN, idle-lockout) + read-only Permissions inspector.
  • Granular permissions mirrored verbatim — catalog + roles + role / staff grants.

Discounts (L2)​

  • Discount builder in Settings: customer discounts that apply to Sales and/or Service, authored in a 3-step builder and assigned per-customer on the customer card (replaces the old A/B/C pricing tier).
  • Per-department assignment, department-validated; the Sales / Service tills (when built) read the customer's assigned discount.

See also​