Skip to content

Commit 7f80d08

Browse files
committed
v2.27.0 — set your own tax rate, and a catalogue that says its name
Added - A custom tax rate: a Custom entry in the country picker opens a panel for the rate, the currency code and the symbol. The rate alone would not have covered the case -- a country absent from the table would have kept a euro sign on every price. Resolved through a sentinel checked BEFORE the table lookup, because an unrecognised code does not fail, it silently applies 20%. - Canada, all 13 provinces and territories, with the combined rate a buyer actually pays and CA$ as the symbol. Data only: the picker matches its code as an opaque string, so a per-province key needed no selector and no migration. Rates from the CRA table, not from the request -- Nova Scotia had moved to 14%. Requested in #23. - Bambu Lab A2L and X2D: both were in the catalogue but reached no tutorial, and neither DevModel code was mapped, so either arrived as 'Select Printer'. Codes and groupings read off Bambu Studio's own resource files. - A Wiki button in the community footer, contributed in #24. Changed - The 'Search' view is called 'Catalogue'. Every identifier around it already said so; only the label the user reads disagreed. - Scanning a code in the inventory lands on ONE spool. It matched by substring, and one brand's range shares a prefix that returned 27 rows. - A rack shelf can be four spools deep. Fixed - Product images: a load queue, a rendition that fits the slot, two retries. 163 of 166 images were fetched while 24 were on screen; the CDN speaks HTTP/2, so the browser's per-origin cap no longer throttles anything. And a photo that missed its turn stayed a flat colour for the whole session, though every failed URL loaded on a second request. - The printer brand picker opened BEHIND an open printer card -- two layout functions docking to the same edge with no knowledge of each other. - In dark mode every community notification wore a black logo, painted with --surface, white by coincidence rather than by intent. - A Bambu H2C reporting its second model code was not recognised.
1 parent 669842b commit 7f80d08

26 files changed

Lines changed: 19511 additions & 397 deletions

‎CHANGELOG.md‎

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

66
---
77

8+
## v2.27.0 — 2026-09-12
9+
10+
### Added
11+
12+
- **Custom tax rate.** A *Custom* entry at the foot of the account's country picker reveals a panel carrying three fields — rate, ISO 4217 currency code, symbol — each with a label and an example. The rate alone would not cover the case it exists for: a user whose country is absent from the table would keep a euro sign on every price, so the currency travels with it. Resolution goes through a sentinel code tested BEFORE the table lookup, because `_vatEntryFor()` does not find `CUSTOM` and falls back to France — an unrecognised code does not fail, it silently applies 20 %. Values are clamped and shaped at READ time rather than trusted from the document (rate to 0–100, currency to at most three letters), since `currency` also reaches the telemetry roll-up, which expects ISO 4217. Field seeding skips whichever input has focus and commits on blur, so a save never rewrites what is being typed. Stored as three flat fields on `users/{uid}` beside `vatCountry`; no rules change, that document is not field-whitelisted — `renderer/inventory.{html,js}`, `renderer/css/60-modals.css`, `renderer/locales/*.json`.
13+
- **Canada, with CAD and a per-province rate.** All 13 provinces and territories. No province selector was built and none was needed: `_vatEntryFor()` matches `code` as an opaque string, never parsing it or assuming two letters, so `CA-QC` is as valid a key as `FR`, and the picker's existing `label · rate% · symbol` template renders `Canada (Quebec) · 14.975% · CA$` unchanged. Data-only: `vatCountry` is a free field on `users/{uid}`, and the telemetry's `country` is derived from the locale rather than from this, so a sub-national code cannot reach the roll-up that expects ISO-2. `rate` is the COMBINED rate the buyer pays — Canada has no national one, GST being federal with HST, PST, RST or QST layered per province — and a new `note` field records each rate's composition. Rates taken from the CRA rate table rather than from the request; Nova Scotia had moved 15 % → 14 % on 2025-04-01. Symbol is `CA$`, not `$`. Requested in #23 — `data/vat-rates.json`.
14+
- **Wiki button in the sidebar's community footer**, linking to `wiki.tigersystem.io`. Driven by the shared `COMMUNITY` table, so the badge, the one-shot notification and the per-account `wikiSeen` flag come from the table entry itself rather than from new wiring. Contributed in #24 — `renderer/inventory.{html,js}`, `renderer/css/{00-base,10-settings,20-friends,70-detail-misc}.css`, `renderer/locales/*.json`.
15+
- **Bambu Lab A2L and X2D.** Both were in the printer catalogue but reached no connection tutorial, and the X2D carried an X1C photograph. The A2L joins the A-series walkthrough, the X2D the shared X / H / P one. Both are now resolved on discovery: the `DevModel` codes `N9` (A2L) and `N6` (X2D) were unmapped, so either machine arrived as "Select Printer" with a "?" thumbnail and no camera transport selected. Codes read from Bambu Studio's own `resources/printers/<CODE>.json`, whose `display_name` and `printer_series` also confirm the tutorial grouping (`series_n` / `series_x1`) and the camera transports (A2L advertises a local liveview like an A1 → JPEG/TCP; X2D none, like a P2S → RTSP). No serial prefix is recorded for either — a prefix can only be read off real hardware, and the code is the path both discovery and the cloud take — `data/printers/bbl_printer_models.json`, `renderer/printers/bambulab/{tutorial.json,probe.js}`.
16+
17+
### Changed
18+
19+
- **The "Search" view is renamed "Catalogue"**, and the i18n key with it (`viewGroupSearch` → `viewGroupCatalog`). Every identifier around it already said catalogue — `vtgCatalog`, `catalogGrid` / `catalogTable`, `_isCatalogMode`, "From Catalogue" — only the user-facing label disagreed — `renderer/inventory.html`, `renderer/locales/*.json`.
20+
- **Inventory search resolves a scanned code to a single spool.** `sku` and `barcode` were already in the haystack but matched by plain substring: Rosa3D's range shares the prefix `590775313`, so that scan returned 27 rows. A query that looks like a code (≥ 6 characters, contains a digit, no spaces) is now matched exactly first against SKU, barcode and UID, and only exact hits are returned. The predicate that decides "looks like a code" moved above both searches so the inventory and catalogue rules cannot drift. With no exact match the ordinary search still runs; anything under six characters (`1000`, `220`, `1.75`) is never treated as a code — `renderer/inventory.js`.
21+
- **`searchPlaceholder` now names SKU and barcode** — both had been searchable all along with nothing to say so — `renderer/locales/*.json`.
22+
- **A rack shelf can be four spools deep**, up from three. The ceiling lived in three places and only one was a number: the stylesheet ENUMERATED the rows, so a fourth would have been drawn flat on the others — no offset, no shadow — and silently, since an unmatched selector fails nothing. The renderer writes each row's index as `--plane` and one rule places any depth from it, reproducing the enumerated ladder because the shelf above depends on that exact scale to paint over the row below — `renderer/inventory.js`, `renderer/inventory.html`, `renderer/css/30-racks.css`.
23+
- **X1E shows an X1E** in the add form instead of an X1C; its artwork had been unused in the folder since the model was added — `data/printers/bbl_printer_models.json`.
24+
- **Bundled TigerTag database refreshed** — 13 239 products, 65 brands. Two `id_version.json` rows were renamed upstream (`TIGER_TAG_MAKER_V1.0` → `TIGER_TAG_V1.0`, `TIGER_TAG_PRO_V1.0` → `TIGER_TAG_PLUS_V1.0`); nothing matches on those strings and the ids and signing keys are unchanged, but `docs/rfid-vendors/tigertag.md` quoted the old ones — `assets/db/tigertag/*`, `docs/rfid-vendors/tigertag.md`.
25+
26+
### Fixed
27+
28+
- **Product images: a load queue, a rendition that fits the slot, and two retries.** Three faults on one path. (a) BURST — opening the catalogue mounts ~166 cards at once and `loading="lazy"` does not throttle it: measured, 163 of 166 were fetched while 24 were on screen. Over HTTP/1.1 the six-connections-per-origin cap did this for free; the CDN speaks HTTP/2, where each request is a concurrent stream on one connection. An IntersectionObserver (600 px margin) decides when an image is wanted and a queue caps six in flight — a cap adapts where a fixed inter-request delay cannot. (b) RENDITION — the CDN serves seven square sizes (`icon` 16 px … `master` 1024 px) selected with `&size=`, and the endpoint defaults to `large`; the app took the default everywhere, spending a 512 px image on a 28 px table chip. Nine call sites now request the first rendition covering their slot at the screen's pixel density, rounding up. A grid card measures 326 device px and still legitimately asks for `large`. Only `cdn.tigertag.io/img` links are rewritten; the two detail-panel images keep `large` deliberately. (c) GIVE-UP — the three sites drawing a photo hid or removed it on the FIRST error, making a momentary failure permanent for the session; every failed URL measured here loaded on a second request in 223–636 ms. Two retries backing off 400 ms / 1400 ms, then the placeholder, on a byte-identical URL so the CDN cache still answers. `load` and `error` do not bubble, hence capture listeners, which also lets one rule replace three inline handlers; a MutationObserver adopts images wherever they appear rather than relying on five render sites to call a scan — `renderer/inventory.js`.
29+
- **Opening the printer brand picker while a printer's side-card was open put the picker BEHIND it**, invisible, with nothing to indicate the click had registered. The two are laid out by functions unaware of each other — `_syncPrinterAddPanels()` for the picker and its choice cards, `_syncPanels()` for the printer/spool/container stack — both docking to the same right edge. The picker now closes that stack on open, edit form included, which also tears down the panel's camera rather than leaving it streaming behind another card — `renderer/inventory.js`.
30+
- **In dark mode every community notification wore a black logo on its brand colour.** The glyph was painted with `--surface` (the card background): `#ffffff` in light, `#171a22` in dark — white by coincidence, not by intent, while the comment two lines above claimed "white logo". Now `--on-accent`, the fixed-polarity token for content sitting on a filled colour, already used by the low-stock chip below and by the sidebar buttons — which is why those were correct in both themes. Discord, GitHub, MakerWorld and Shop were all affected — `renderer/css/20-friends.css`.
31+
- **A Bambu H2C reporting `O1C2` was not recognised.** The machine answers with either of two model codes and only one was mapped, so the same printer resolved to its catalogue entry or to "Select Printer" depending on which it sent — and unresolved means a "?" thumbnail and no camera transport — `renderer/printers/bambulab/probe.js`.
32+
- **`wikiClickedAt` was silently rejected by the Firestore rules.** The community buttons stamp a `*ClickedAt` field on `users/{uid}/telemetry/studio`, a field-whitelisted collection where the new field was unlisted; `hasOnly()` evaluates the POST-MERGE document, so the write failed wholesale rather than dropping one field, and the client swallows the rejection. Not user-visible — the nudge's `wikiSeen` flag lives on `users/{uid}`, which is not whitelisted — but the click analytics were lost. Fixed and deployed in the backend repo's `firestore.rules`; no app-side change.
33+
34+
---
35+
836
## v2.26.1 — 2026-08-31
937

1038
### Fixed

‎CLAUDE.md‎

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,15 @@ set the GitHub repo variable `TRUSTED_SIGNING_CERT_PROFILE` → update `.github/
3232

3333
**This file is committed and PUBLIC.** Never write an absolute path into it — a
3434
`/Users/…` line publishes the account name and the folder layout to GitHub, and
35-
it is wrong on every other machine anyway. Refer to a sibling repo by **name**
35+
it is wrong on every other machine anyway.
36+
37+
**Never write an account EMAIL ADDRESS into any file of any repo** — not in a
38+
doc, a comment, a commit message or a runbook, and least of all the shared admin
39+
account the Firebase / Vercel / GCP consoles are signed into. Name the account
40+
by its handle or its role ("the team Owner account") instead: that is what a
41+
reader needs to act, and it identifies nothing that can be phished or enumerated.
42+
This is not theoretical — one slipped into a PUBLIC repo's runbook and had to be
43+
removed afterwards, which does not remove it from the history. Refer to a sibling repo by **name**
3644
(`TigerTag_Firebase_Backend`) or relatively (`../TigerTag_Firebase_Backend`).
3745

3846
Where the checkouts actually live, which clone is the live one and which are
@@ -436,6 +444,52 @@ Each brand under `renderer/printers/<brand>/PROTOCOL.md` is a **self-contained a
436444
## API base
437445
`https://cdn.tigertag.io` — endpoints: `/healthz/`, `/setSpoolWeightByRfid?ApiKey=&uid=&weight=`
438446

447+
### Product images — ask for the size you are going to paint
448+
449+
`/img?id=<productId>[&v=<n>][&size=<name>]` serves **seven square renditions** of
450+
every product photo. Source of truth: the `SIZES` map in the backend repo's
451+
`functions/index.js`.
452+
453+
**The byte figures below are JPEG.** The proxy negotiates on the `Accept` header
454+
and serves **AVIF** to Chromium — measured, 25 requests out of 25 — so the real
455+
transfer is smaller. The ratios between rungs still hold; treat the absolute
456+
numbers as an upper bound.
457+
458+
| size | px | bytes | typical use |
459+
|---|---|---|---|
460+
| `icon` | 16 | 429 B | |
461+
| `thumb` | 32 | 663 B | |
462+
| `small` | 64 | 1.4 kB | list / table chips (28 px) |
463+
| `compact` | 128 | 3.4 kB | catalogue + reorder thumbs (50-64 px) |
464+
| `medium` | 256 | 9.7 kB | |
465+
| `large` | 512 | 33 kB | **the default** — grid cards (163 px) |
466+
| `master` | 1024 | 101 kB | zoom / lightbox, **on demand only** |
467+
468+
**The endpoint defaults to `large`, so omitting `size` costs 33 kB every time** —
469+
that is how 28 px table chips came to download 512 px images. In the renderer,
470+
`cdnImg(url, cssPx)` picks the first rendition that covers the slot at the
471+
screen's pixel density, rounding UP (oversized is invisible, undersized is
472+
visibly soft); `_cdnSizeFor` holds the ladder. It rewrites ONLY
473+
`cdn.tigertag.io/img` links — a user's uploaded photo is any URL at all, and
474+
`resolvedImg` may hand back a `data:` URI.
475+
476+
Two properties worth knowing before optimising anything here:
477+
478+
- **Every image is two requests.** `/img` answers **302** to Firebase Storage.
479+
The redirect is `immutable, max-age=1y`, so it is a cold-start cost — but it
480+
doubles the request count exactly when a list first paints.
481+
- **Chromium's HTTP cache already does the heavy lifting.** The images are
482+
`immutable, max-age=1y`, so a second visit costs nothing — measured on a cold
483+
reload of the catalogue: 25 images, 25 disk-cache hits, 0 bytes, the 302 not
484+
even replayed. The app's OWN cache (`preCacheImages` → `img_cache/`) only
485+
warms the INVENTORY; the difference is that ours is permanent and the
486+
browser's evicts. Do not describe the catalogue as "uncached".
487+
- **The CDN speaks HTTP/2**, so the browser's six-connections-per-origin cap
488+
does NOT throttle a burst; all requests become concurrent streams on one
489+
connection. The renderer puts the ceiling back itself (IntersectionObserver +
490+
a six-in-flight queue, see the *Product images* block in `inventory.js`).
491+
An unknown `size` returns **400**, and `Accept: image/avif` is honoured.
492+
439493
---
440494
441495
## Firebase integration

0 commit comments

Comments
 (0)