Skip to content

Commit d69b1c1

Browse files
committed
v2.16.0 — data-loss fix, bundled catalogue, self-maintaining spools
Fixes a data-loss bug in the chipless id migration: syncSpoolMirrors used set(merge), so it could resurrect a CLOUD_ document just after the migration deleted it, as a two-field husk; the next snapshot re-ran the migration on the husk and batch.set — a full overwrite — replaced the correctly migrated document with it. One account lost the identity of 136 spools. Three independent guards now close it, and twin pointers are finally retargeted. Also ships the product catalogue inside the app (searchable offline on first launch), makes catalogue-tied spools reconcile themselves as the catalogue evolves, and ratifies the material-swatch colour convention across every surface that draws a spool.
1 parent 5b93e65 commit d69b1c1

37 files changed

Lines changed: 70196 additions & 409 deletions

‎.env.example‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# electron-builder will read these env vars automatically when building macOS targets.
1111

1212
# Path to the downloaded .p8 file (absolute path recommended)
13-
APPLE_API_KEY=/Users/benglut/Documents/AppleKeys/AuthKey_XXXXXXXXXX.p8
13+
APPLE_API_KEY=/absolute/path/to/AuthKey_XXXXXXXXXX.p8
1414

1515
# Key ID — 10 alphanumeric chars shown next to the key name in App Store Connect
1616
APPLE_API_KEY_ID=XXXXXXXXXX

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,6 @@ acu-cam-mqtt.json
149149
# not committed (commercial SDK; it's an npm dependency instead)
150150
renderer/lib/agora/
151151
tmp/
152+
153+
# Machine-local agent reference (absolute paths — must never be pushed)
154+
CLAUDE.local.md

‎CHANGELOG.md‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,38 @@ Versions follow [Semantic Versioning](https://semver.org/).
55

66
---
77

8+
## v2.16.0 — 2026-07-28
9+
10+
### Added
11+
12+
- **The product catalogue ships with the app.** `assets/db/tigertag/id_catalog.json` (~3 000 products, 1.5 MB) is now refreshed by `db_update.py` alongside the reference tables, so one command keeps the whole bundled database current. A fresh install has a searchable catalogue on FIRST LAUNCH, offline, instead of an empty Search view until the first sync lands; the bundle is read only when nothing is cached and `_catalogFetchedAt` stays 0, so a real sync still happens as soon as there is a network — it is a floor, not a substitute. Keeping it in git also makes the catalogue's own evolution reviewable. The fetch needs its own path inside `db_update.py`: a paged POST, and no `products` key in `all/last_update` to compare against — so it downloads in full and rewrites only when the normalised output (sorted by id, keys sorted) actually differs, which keeps a 1.5 MB file from churning the repo — `assets/db/tigertag/db_update.py`, `assets/db/tigertag/id_catalog.json`, `renderer/inventory.js`.
13+
- **The catalogue keeps TigerData+ and TigerTag+ spools up to date by itself.** A new `catalogSyncedAt` stamp records when a spool was last reconciled with the catalogue — a field of its own, because `last_update` already means "when the chip was read or written" and `updatedAt` moves on every write including a weight change. After each catalogue sync, `_catalogStaleRows` compares it against the product's own `updated_at` (absent = never changed since creation = current) and `_catalogRefreshStale` refreshes the ones that moved: in the background, sequentially, capped per sync, and never in a friend's inventory (guarded at both the scan and the write — a friend's account is strictly read-only). Spools predating the mechanism carry no stamp and are swept once, which reconciles the installed base. The refresh goes through the same path as the manual button, so the consequence is decided in one place: the record updates either way, and the chip-pending flag is raised only when the change touched something the chip carries — a TigerData+ has no chip and updates in complete silence. `_refreshApiData` gained a second fetch route for that: its IPC builds the URL from the chip UID (`BigInt("0x" + uid)`), which throws on a chipless id, so chipless spools go through `lookupProduct(id_product)` as their creation did — `renderer/inventory.js`.
14+
- **`series` and `name` come from the catalogue instead of being guessed.** The list endpoint returns them as their own fields; Studio split `title` on the last " - ", which gets a dashed colour name backwards — "PLA Basic - CMYK - Magenta" is series `PLA Basic` + name `CMYK - Magenta`, not series `PLA Basic - CMYK`. Measured on a live 1 000-product page: the split disagreed with the API on 2. The split survives only as a fallback for a cache written by an older build — `renderer/inventory.js`.
15+
- **`playground/material-swatch/`** — a standalone dependency-free page that ratifies the colour convention against the shipped code, same status as the `.ttag` fields editor. It crosses all 17 colour cases with the 11 surfaces that paint a spool (swatches 56/28/15/13/12 px, table thumbnail, grid card, side-card illustration, catalogue row, both rack slot views, twin link, Add-Product preview), each case carrying a live colour picker; it links the SHIPPED stylesheets and copies the branch logic verbatim, so what it shows is what the app draws. Internal tool — `playground/material-swatch/index.html`.
16+
17+
### Changed
18+
19+
- **Spool colours follow one ratified convention instead of per-surface habits — everything is a camembert except a ramp, and every ramp is at 135°.** Two shared helpers own it (`_pieSplit`, `RAMP_ANGLE`) and every surface goes through them, including the Add-Product preview, which used to open-code its own gradients: a bicolor drew a 50/50 linear split there and a 180°/180° conic in the inventory — the same vertical line MIRRORED, so the spool swapped sides between the preview and Save. Bicolor is no longer a special case but a consequence: two equal conic sectors put the boundary on the vertical axis, so the vertical split is guaranteed on a round swatch, a square tile and a clipped fill bar alike. Ramps (rainbow in all four variants, and the catalogue's declared `gradient` type) moved from 90° to a shared 135° diagonal. Ratified as an ecosystem-wide convention whose canonical home is **TigerSystem-Docs** (`docs/developers/material-swatch.md`) rather than this repo, since the Hub, the mobile app and third parties implement it too — `renderer/inventory.js`, `docs/MATERIAL-SWATCH.md`, `CLAUDE.md`, `playground/material-swatch/index.html`.
20+
- **"To order" moved from Favorites to Lists.** Favorites answers "what do I own"; the To-order cart answers "what am I about to buy" — the same question the wishlists ask, and they feed it. Nothing else moved: the button keeps its id, so the sliding indicator lights the Lists segment on its own, the boot restore still finds it, and it still hides in a friend view — `renderer/inventory.html`.
21+
- **"From the catalogue" lands in the layout you were already in** — inventory grid opens the catalogue grid, inventory list opens the catalogue list. It always opened the list before, so leaving a wall of cards dropped you into rows and the Search segment read as a different feature rather than the same shelf seen from the catalogue side — `renderer/inventory.js`.
22+
- **The catalogue thumbnail's TigerTag watermark was faded while every other one was solid** (`opacity: .55` against 1). All watermarks are now identical: top-right, full opacity, sized as a percentage of the tile — `renderer/css/70-detail-misc.css`.
23+
24+
### Fixed
25+
26+
- **DATA LOSS — the chipless id migration could destroy the spool it had just migrated.** `syncSpoolMirrors` was the one inventory write using `set(merge)`, so it could RESURRECT a `CLOUD_` document microseconds after `migrateOneSpoolCloudToTigerData` deleted it — as a two-field husk carrying only `productKey` and `protocol`. The next snapshot saw a `CLOUD_` id again, re-ran the migration, and `batch.set(newRef, …)` — a FULL overwrite, not a merge — replaced the correctly migrated document with the husk. One account lost the brand, material, colour, weight and price of **136 spools** in five seconds (2026-07-23T16:56Z, on v2.14.0; 136 kg and 563 € off its books). Because the husks all hash to the same `productKey` they collapsed into a single stack of 136 blank spools, which is how it surfaced — and because the material count barely moved (317 → 318) it read as an addition rather than a loss until the per-document `createTime` was checked. Three fixes, each sufficient alone: the mirror uses `update()`, which fails on a missing document instead of creating one; the migration REFUSES to propagate a source carrying no identity field (`id_brand`/`id_material`/`id_type`/`id_product`/`weight_available` all absent), dropping the husk instead; and it now retargets twin pointers — its own, and those aimed at it, queried rather than read from the local snapshot — which the old code omitted on the strength of a comment claiming a chipless spool can have no twin, disproved by two twinned chipless spools in that same account — `renderer/inventory.js`.
27+
- **A chip re-write was asked for when nothing on the chip had changed.** The staleness test ran off a hand-written list of 18 fields that included `name`, `series`, `sku` and `barcode` — none of which are on the chip — so a brand renaming a product sent the user to the reader to write bytes identical to the ones already there. It now uses `TTAG_ON_CHIP_FIELDS`, the ratified `On chip` column of `docs/TTAG-FIELDS.md`, and `npm run docs:check` fails the commit if the two drift in either direction — `renderer/inventory.js`, `scripts/check-docs-drift.mjs`.
28+
- **The `.ttag` contract demanded three fields no document has ever had, so NOTHING was exportable.** `TTAG_REQUIRED_NUM` was written from the names of the REFERENCE FILES rather than from the document schema: `id_measure_unit` is the file, the field is `id_unit`; `id_diameter` is not a field at all (the diameter id travels in `data1`, which Studio reads as `id_diameter ?? data1` and writes back as `data1`); and `id_version` is derived from `id_tigertag`, never stored. Every real material therefore failed validation and was counted as refused — silently, since the user only sees a rejection count. Caught by running the shipped validator against real inventory documents (0/168 accepted before, 166/168 after). The contract was re-ratified in `playground/ttag-fields-editor` and `docs/TTAG-FIELDS.md` regenerated from it — `renderer/inventory.js`, `playground/ttag-fields-editor/index.html`, `docs/TTAG-FIELDS.md`.
29+
- **`db_update.py` could destroy a good reference file, and was still GPL.** A 200 response was treated as proof of a good payload — an API answering `[]` during a migration, `{"error": …}` on a soft failure, or an HTML page from a proxy would have been written straight over live data — and `open(path, "w")` truncates BEFORE writing, so a crash or a full disk left a truncated JSON the app cannot parse. Added a non-empty-array guard and temp-file + `fsync` + `os.replace` writes, and surfaced our own errors as a message rather than a traceback (the script runs in CI, where the message is the diagnosis). Verified by attack: a sandbox holding a good file survives 404, 500, an HTML page, an empty array, an error object and a bare string, leaves no `.tmp`, and still accepts a valid payload. The header moved from GPL v3 to **Apache-2.0**, matching the public Guide and the repo's own MIT licence — GPL on a sync script contradicts "implementing TigerTag requires no licence" — `assets/db/tigertag/db_update.py`.
30+
- **`isColorDark` read every 8-digit colour as light.** Its regex tried `{6}` before `{8}`, so on an `#RRGGBBAA` it matched the first six digits and then failed the trailing `\b` (the alpha digits are word chars) — no match at all, and the helper's "unknown → light" default kicked in. `#000000FF`, the raw form the product catalogue serves, therefore chose the black-outline logo to draw on a black spool. Not reachable from Studio's own screens today (`colorBg` normalises to six digits before every call), but the helper is shared — TigerScale imports it, the Hub reimplements it. Alternatives reordered longest-first and the alpha is now dropped for both `#RRGGBBAA` and `#RGBA`; found by TigerHub, which hit it live on unnormalised catalogue colours — `renderer/inventory.js`.
31+
- **Bulk delete could fire at spools the filter had hidden.** The count on the bar is the whole selection, but the table only shows what passes the current filter — so selecting everything and then searching left "Delete (143)" sitting under a single row, aimed at 142 spools the user could not see. The selection is kept on purpose (filtering is how you build one), so the bar now says the quiet part: an amber chip naming how many of the selected rows are hidden, shown only when the two counts differ — `renderer/inventory.js`, `renderer/inventory.html`, `renderer/css/70-detail-misc.css`, i18n `bulkHiddenWarn`.
32+
- **Opening a filter dropdown shoved the whole view sideways.** `_enhanceSelect` brought the selected option into view with `scrollIntoView`, which scrolls EVERY scrollable ancestor — and `overflow: hidden` still scrolls programmatically, so `#card-inv` slid across and clipped its own left edge. The list now scrolls itself, and the popup flips to right-aligned when left-aligning would push it past the card (measured, since how far the last filter sits depends on the locale's label widths) — `renderer/inventory.js`, `renderer/css/70-detail-misc.css`.
33+
- **The Search list lost its column header on scroll.** `.cv-body`, the only element between the view and the table, carried no rules at all — so `flex: 1; min-height: 0` on the table wrap resolved against a plain block, the wrap grew to its full content height, and the sticky `<thead>` sat pinned inside a box taller than the screen, sliding away with the rows while the OUTER view did the scrolling. Making the body a flex column that fills the view lets the bordered box actually be the scroller, the same recipe Inventory and Favorites already use. Scoped to `--table`: in grid mode the body IS the `.inv-grid`, and the cards must still size to their content — `renderer/css/70-detail-misc.css`.
34+
- **Nothing looked selected in the view selector at first launch.** The selector lives inside `#card-inv`, which is `display: none` until sign-in completes — so at boot every button measures 0 px wide, the sliding bubble hides itself, and nothing put it back when the card finally appeared. An earlier attempt placed the bubble in the boot block, which is exactly the moment the card is still hidden; it was verified in a harness that had already revealed the card, so the test could not see the bug. `renderInventory` now positions it — the first moment the card is reliably on screen — `renderer/inventory.js`.
35+
- **Closing the Product card left the Search row still highlighted.** The link ran one way — picking a row opened the card, but shutting the card (its ✕, opening another, leaving the view) left the row lit, pointing at a panel that was no longer there. `closeProductCard` now releases the selection, so every route out of the card agrees. Split into a side-effect-free painter plus a clear, which is what stops the two from calling each other — `renderer/inventory.js`.
36+
- **The repo published the maintainer's home folder to GitHub.** Sixteen absolute `/Users/<account>/Documents/…` paths sat in five committed files — `CLAUDE.md`, `PERF_AUDIT_v1.8.13.md`, `docs/REVIEW-BRIEF.md`, `docs/spool-grouping-prompt.md` and `.env.example` — leaking the account name and the local checkout layout on a public repository, and wrong on any other machine anyway. Committed docs now name a sibling repo (`TigerTag_Firebase_Backend`) or reach it relatively. The machine-specific half moved to a new **gitignored `CLAUDE.local.md`** — where each checkout lives, which clone is live and which are stale decoys, and the per-repo commit policy — with `CLAUDE.md` carrying the standing rule that it is public and must never contain an absolute path. The other five Tiger repos were swept and are clean — `CLAUDE.md`, `CLAUDE.local.md`, `.gitignore`, `.env.example`, `PERF_AUDIT_v1.8.13.md`, `docs/REVIEW-BRIEF.md`, `docs/spool-grouping-prompt.md`.
37+
38+
---
39+
840
## v2.15.0 — 2026-07-27
941

1042
### Added

0 commit comments

Comments
 (0)