Skip to content

Commit 2fbbc61

Browse files
committed
v2.11.2 — Twin-spool repair now resolves conflicts, not just gaps: when a spool's two linked NFC tags had drifted apart (a different container or custom image saved on each before mirroring existed — some differing in container weight, so the displayed net weight depended on which chip showed), the one-shot repair now treats the twin the inventory actually displays as the source of truth and aligns the other chip to it (a hollow displayed chip is still filled from its twin). Re-armed as tigertag.twinFix.v2.*; timestamp-neutral.
1 parent f4c3c93 commit 2fbbc61

8 files changed

Lines changed: 86 additions & 46 deletions

File tree

‎CHANGELOG.md‎

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

66
---
77

8+
## v2.11.2 — 2026-07-15
9+
10+
### Changed
11+
12+
- **The twin-spool repair now also resolves conflicts, not just fills gaps.** v2.11.1's one-shot repair copied a spool's custom data (image, container, rack, tags, note) onto its twin's *hollow* chip, but left genuine conflicts — the two chips carrying a *different* container or image, each set before the mirror existed — untouched. Some of those differed in container **weight**, so the displayed net weight depended on which chip the inventory happened to show. The repair now treats the twin the inventory actually **displays** (the one `deduplicateTwins` keeps — chosen before the sort, so independent of the user's sort column) as the source of truth: its fields overwrite the other chip's, resolving conflicts to exactly what the user sees (a hollow displayed chip is still filled from its twin). Re-armed for accounts that already ran the earlier pass (`tigertag.twinFix.v2.*`); still timestamp-neutral (never writes the chip `timestamp` or `updatedAt`) — `renderer/inventory.js` (`reconcileTwinFields`).
13+
814
## v2.11.1 — 2026-07-15
915

1016
### Added

‎FEATURES.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Tiger Studio Manager — Feature Catalogue
22

3-
Tiger Studio Manager is the Electron desktop companion to the TigerTag ecosystem: a filament-inventory manager built around RFID/NFC-tagged spools ("TigerTag" chips), a fully-digital "TigerCloud" tier, live 3D-printer integration across six brands, physical storage/rack management, a Firebase-backed social layer (friends, shareable wishlists, public profiles), and companion hardware (TD1S color sensor, TigerScale, TigerPOD dual-reader stand). This document catalogues every **shipped** feature, grouped by domain, current as of **v2.11.1**. Per-version release detail lives in `CHANGELOG.md`; forward-looking / in-progress work lives in `ROADMAP.md`.
3+
Tiger Studio Manager is the Electron desktop companion to the TigerTag ecosystem: a filament-inventory manager built around RFID/NFC-tagged spools ("TigerTag" chips), a fully-digital "TigerCloud" tier, live 3D-printer integration across six brands, physical storage/rack management, a Firebase-backed social layer (friends, shareable wishlists, public profiles), and companion hardware (TD1S color sensor, TigerScale, TigerPOD dual-reader stand). This document catalogues every **shipped** feature, grouped by domain, current as of **v2.11.2**. Per-version release detail lives in `CHANGELOG.md`; forward-looking / in-progress work lives in `ROADMAP.md`.
44

55
---
66

‎WORKLOG.md‎

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,11 @@
1-
# Worklog — v2.11.1
1+
# Worklog — v2.11.2
22

33
## Added
4-
- **Xano chip-event analytics — Studio now reports physical-chip events like the mobile app does.** Reads, writes and chip resets performed with a TigerPOD / NFC reader are reported to the TigerTag Xano analytics table (`api.tigertag.io/api:tigertag/analytics/tag`), so Studio and Tiger RFID Connect land in the **same table with the same shape**. Five operations, mirroring the mobile vocabulary: `read` and `online` (chip scanned + decoded — see the routing note below), `write` (chip programmed — fired per chip actually burned AND verified, where the mobile fires once per *batch* and so under-reports multi-chip writes), `reset` (Format → back to the TigerTag Init state) and `nfc_init` (Recycle → blanked to a plain NFC tag). Studio never emits `init` (mobile's "a foreign chip became a TigerTag") — it has no such flow.
5-
- **A scan routes to one of TWO endpoints, and the choice decides whether the catalogue product is recorded at all.** A TigerTag+ whose chip carries a real catalogue product emits **`online`** to `product/get?uid=&product_id=` — that endpoint takes `product_id` as a query input and hands it to the same `Analytics Header Data to DB` function, so the row lands WITH the product identity. Everything else (a maker chip, `write`, `reset`, `nfc_init`) goes to `analytics/tag`, which has **no `product_id` field at all** — so those rows can never carry one, by design. Both endpoints read the same base64 `X-Tigertag-Analytics` header (it is literally how `product/get` detects an app call; the function aborts on a `DATA != null` precondition without it). Xano only records the `online` row when `product_id > 500`: ids below that are the reserved range for internal/demo chips, deliberately excluded from analytics. The `product/get` response (the product payload) is ignored — Studio fetches that on its own path; Xano logs the analytics in a Post-Process block, i.e. after responding, so it costs the user nothing.
6-
- **Transport (not ours to change):** a **GET** whose JSON payload is base64-encoded into an `X-TigerTag-Analytics` header, on an unauthenticated endpoint. Sent from the **main process**, because the renderer is a browser context and a cross-origin GET carrying a custom header would trigger a CORS preflight the endpoint doesn't answer. Fire-and-forget by construction (`ipcMain.on`, not `handle`): a dropped event is lost, never retried, and can never block or slow a chip action — same contract as mobile, so server-side counts are a lower bound.
7-
- **60 s dedup per (operation, UID)** — the TigerPOD is a *resting* reader that re-reads the same chip on a loop and would otherwise flood the table. The mobile app needs no such guard (every scan there is a deliberate tap). A different operation on the same chip is never suppressed.
8-
- **UID sent in DECIMAL** (`BigInt("0x"+hex)`) — Xano keys on decimal. Noted for the mobile team: the mobile's `online` operation puts the UID in **hex** into that same field (it reuses a mis-named `uidDecimal` variable), so those rows are mis-keyed in Xano.
9-
- **Privacy:** `Location` is always `"0.0,0.0"` — Studio collects **no position**. A desktop doesn't move, so a precise coordinate would reveal the user's home address while answering no analytical question that `Country` (derived offline from the timezone — no IP lookup, no permission prompt) doesn't already answer; Xano can still resolve the request IP on its side. `Emoji` is deprecated and always sent empty; `Message` is sent, as on mobile. A new `App: "studio"` field tells Studio rows apart from mobile ones, and `Platform` is sent as a product name (`macos` / `windows` / `linux`) rather than Node's kernel names — `darwin` reads like noise next to the mobile's `ios`/`android`, and `win32` is actively misleading (Node returns it on 64-bit Windows too).
10-
— `main.js` (`analytics:tag` IPC + the Xano request), `preload.js` (`sendTagAnalytics`), `renderer/inventory.js` (`_sendTagAnalytics` / `_xanoChipFields` / `_xanoUidDecimal`, wired into `_processNfcScans`, the encode/burn loop and `_wireChipReset`).
11-
- **Server-side stats & history (backend repo) — the foundation of the user + admin dashboards.** New in `TigerTag_Firebase_Backend`: `users/{uid}/stats/current` (rollup) and `users/{uid}/dataHistory/{autoId}` (time-series snapshots, ≤1 point/15 min) plus the global `adminStats/{YYYY-MM-DD}`. Computed **server-side only** — a client-written business figure is untrustworthy (publicly connectable backend) and leaves holes in the curve whenever the app is closed. Architecture is *flag + reconciler, never a scan*: the `onDocumentWritten` triggers on inventory / products / printers / lists do nothing but set `dirty = true` (one write, zero reads — a print job pushing a spool's weight down costs almost nothing), and a 15-min scheduled reconciler queries `collectionGroup("stats").where("dirty","==",true)` so it reads only the accounts that moved (dormant accounts are never billed). It clears the flag *before* recomputing (a write landing mid-recompute re-marks the account → picked up next cycle; at worst one extra recompute, never a lost update) and recomputes the FULL snapshot rather than applying increments (an incremental counter that misses a delta drifts permanently and silently; a full recompute is self-healing). Raw points older than 90 days are downsampled to one per day, pruned lazily per account. Monetary values are always HT; the global rollup keeps `valueHtByCurrency` because summing different currencies would be meaningless. Also documented in `docs/firestore-schema.md` + the backend README. **Deployed** (rules, indexes, 7 functions) and backfilled over all 2 372 existing accounts; a Maintenance panel in the TigerHub admin console (`/admin`) can re-trigger the global rollup or a full rebuild on demand — `firestore.rules`, `firestore.indexes.json`, `functions/index.js` in the backend repo.
12-
- **`protocol` mirrored onto each inventory doc — it's what makes the TigerTag / TigerTag+ split possible server-side.** Same reasoning as `productKey`: the protocol is resolved via `versionName(id_tigertag)` against the TigerTag reference DB, which the backend doesn't have. Counting the RAW `id_tigertag` server-side is not a substitute — only a couple of values map to a known version and the rest are a long tail of unrecognised ids, so it produced ~200 noise keys *and* no TigerTag-vs-TigerTag+ split at all (the Hub could only show "chip vs TigerCloud"). The client now mirrors the resolved string (`TigerTag` / `TigerTag+` / `TigerCloud` / `unknown`) and the backend counts a clean `byProtocol` map, replacing the raw `byVersionId` — `renderer/inventory.js` (`syncSpoolMirrors`, renamed from `syncProductKeys` now that it mirrors two fields), backend `functions/index.js` (`computeUserStats`, `rollupGlobalStats`, the inventory trigger's changed-field list).
13-
- **`productKey` mirrored onto each inventory doc — the join key for the upcoming server-side stats.** The product identity hash (`_productKeyHash`) is built from RESOLVED names (brand / material / aspect come from the TigerTag reference DB), so a backend job cannot recompute it from the raw Firestore fields — it could never join a spool to its `products/{keyHash}` record and the stock value would come out as 0. The hash is now denormalised onto `users/{uid}/inventory/{spoolId}.productKey` (same pattern as the `rfidBackup` mirror flag), so the backend does a plain lookup: no reference DB server-side, no second copy of the hashing logic, no drift when the catalogue changes. Written from the inventory snapshot handler (`syncProductKeys`) rather than the ~10 spool-write sites — one self-healing path covering new spools, identity edits (colour/brand change → new hash) and the one-time backfill of existing spools; it only writes when the stored value actually differs, so the write settles on the next snapshot instead of looping, and it bails until the reference DB has loaded (otherwise rows would hash from fallback labels and persist a wrong key). No rules change needed (`inventory` has no `hasOnly` whitelist) — `renderer/inventory.js` (`syncProductKeys`, `subscribeInventory`).
14-
- **New `cart-plus` icon** (`assets/svg/icons/icon_cart_plus.svg`, `.icon-cart-plus` mask) — a cart with a **+** inside the basket, drawn in the same 24-viewBox / 2px round-stroke style as the existing cart.
154

165
## Changed
17-
- **The "add to To order" action and the "buy at the shop" action no longer share the same cart glyph.** They are fundamentally different — one puts a product in YOUR reorder list, the other opens an external shop page — but both rendered `icon-cart`, so they were indistinguishable side by side (notably on the product card, where they sit inches apart). Split into two glyphs with a clear rule:
18-
- **`icon-cart-plus` (cart +) = "put it in my To order list"** — the three `.flag-toggle--wish` toggles (product card, spool detail panel, grouped-spools side-card), the bulk "To order" button (`#bulkLike`), the `liked` state badge on the spool illustration (`.prod-badge--wish`), and the To-order quick filter (`#wishFilter`). Both icon setters now derive this base (`_setProductFlagBtn`: `field === "liked" → icon-cart-plus`; `_setBulkFlagBtn(likeBtn, "icon-cart-plus")`), including the "no filled variant" check that keeps the glyph identical on/off (the `.active` tint carries the on-state).
19-
- **`icon-cart` (plain) = "go buy it"** — every buy link (`.pv-buy`, `.pc-buy`, `.lv-buy` / `.lv-addlink`, `.det-buy-link`, the reorder card's `#roShopBtn`, the toolbox `btnToolReorderBuy`) plus the purchase-source group headers. The To-order view tab and the empty-cart illustrations keep the plain cart too — they depict the cart itself, not the "add" action.
20-
— `renderer/inventory.js`, `renderer/inventory.html`, `renderer/css/70-detail-misc.css`, `assets/svg/icons/icon_cart_plus.svg`.
6+
- **Twin-field repair now resolves conflicts, not just gaps — the DISPLAYED spool wins.** The v2.11.1 one-shot repair filled a twin's hollow chip but left genuine conflicts (both chips carrying a *different* container or custom image, set on each before mirroring existed) untouched. Real friend data showed 8 such pairs (7 container, 1 image), some with different container *weights* → the displayed net weight depended on which chip the UI happened to show. The reconciler now treats the twin the inventory actually displays (the one `deduplicateTwins` keeps — chosen before the sort, so independent of the user's sort column) as the source of truth: its per-spool fields overwrite the other chip's, resolving conflicts to exactly what the user sees; a hollow displayed chip is still filled from its twin. Flag bumped to `tigertag.twinFix.v2.*` so accounts that already ran the fill-gaps pass get one more pass to settle conflicts; still timestamp-neutral (never writes the chip `timestamp` or `updatedAt`) — `renderer/inventory.js` (`reconcileTwinFields`).
217

228
## Fixed
23-
- **Twin Tiger Tag spool on the Pod: the side-card opened, then swapped to a second card showing "reset" data.** A twin spool = TWO Firestore docs (one per chip UID), independent by design. The per-spool custom fields (custom image, container) were saved on only ONE of the two docs, leaving the twin "hollow"; when a scan opened the card on the hollow chip, it looked reset. (Invisible on Tiger Tag+ — both docs render the same shared catalogue data.) Two-part fix:
24-
- **Root cause — mirror the remaining per-spool fields onto the twin.** Weight, rack placement and tags already mirrored at their write sites; added the same for **custom image** and **container** (id + weight) via a shared `_updateSpoolTwinned(r, patch)` helper, so both chips of one physical spool now always carry the same custom data. This also fixes the grid/stats, which read the raw docs.
25-
- **Symptom — stop the card swap.** The pod's two readers fire a bare `rfid-uid` each ~ms apart; before the twin link resolves (~1.5 s later, in `_processNfcScans`) the anti-duplicate guard couldn't tell the two chips were one spool, so the second swapped the card to its own doc. Widened the guard to also treat the scan as the same spool when the scanned chip shares the open card's product identity (`_spoolGroupKey`, available on both docs immediately) — gated to the twin-arrival burst (< 1.6 s of the card being scan-opened) so scanning ANOTHER physical spool of the same product later still opens its own card. No added delay; instant open preserved.
26-
- **Existing data — a one-shot repair (`reconcileTwinFields`).** New spools mirror going forward, but twins configured before this fix stay inconsistent. A reconciler fills each twin's hollow chip from its configured half across image / container / rack / tags / note — **fill-gaps only** (copies a field only from the twin that HAS it to the one that doesn't; never overwrites a present value, so no data loss and no "which is right?" guess; a genuine both-present conflict is logged, not touched). Deliberately NOT real-time: the localStorage-gated check runs only on the FIRST server snapshot of a session and the actual reconciliation runs **once ever per account**; it never re-runs on later snapshots. An admin **Debug → "Repair twin spools"** button forces it again (reads the in-memory inventory, writes only where a gap exists — the changed fields aren't in the stats trigger's watch list, so no backend churn). The last run's outcome (pairs checked / docs filled + timestamp) is persisted and shown in the Debug panel, so the silent first-launch pass is visible after the fact.
27-
— `renderer/inventory.js` (`_updateSpoolTwinned` + custom-image / container write sites, `_consumeScanOpen`, `reconcileTwinFields` + one-shot gating), `renderer/inventory.html` (debug-panel Tools button).
28-
- **A friend's "Stock value" was far too low — it only counted the products you happened to own too.** `_getProduct(r)` always resolves against `state.products` (MY records), but in friend-view the rows on screen are the FRIEND's spools. So the value loop (`price × remaining-weight fraction`) skipped every spool for which I had no priced product of my own (`if (!Number.isFinite(price)) continue`), collapsing the total to the *intersection* of our two shelves — likewise the price column and price sorting showed "-" for the friend's spools. Added `_displayProduct(r)`, which resolves the product record DESCRIBING the row on screen (`state.friendProducts` in friend-view, `state.products` otherwise), and switched the six "displayed price" sites to it: the stock-value stat, price sorting (rows + groups), the table price cell, and the row/group render signatures. `_getProduct()` stays MINE everywhere it must (❤/★ toggles, min stock, note, tags, bulk flags, the owner-only telemetry snapshot) — the toggles write to my own account even while browsing a friend. Second half of the bug: the friend-products snapshot refreshed only the open card and the favorites view, so when it landed *after* the inventory render the value stayed stuck at its pre-products figure — it now re-runs `renderStats()` and the inventory render too — `renderer/inventory.js` (`_displayProduct`, `renderStats`, `_priceCell`, `_rowSignature`, `_groupHeaderSig`, `subscribeFriendProducts`).
299

3010
## Removed
3111

‎data/release-notes/v2.11.2.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Tiger Studio Manager v2.11.2
2+
3+
A follow-up to the twin-tag spool cleanup from v2.11.1.
4+
5+
## Fixed
6+
7+
- Finished tidying up spools that carry two linked NFC tags: when the two tags had drifted apart — a different box or picture saved on each — they now agree on whatever the inventory shows you, including the box weight (so the remaining weight reads the same either way).
8+
9+
---
10+
11+
📖 **[Full changelog](https://github.com/TigerTag-Project/TigerTag-Studio-Manager/blob/main/CHANGELOG.md)** · ⬇️ Grab the installer for your OS from the **Assets** below (macOS `.dmg`, Windows `Setup .exe`, Linux `.AppImage`).

0 commit comments

Comments
 (0)