Repository navigation
Conversation
…e-less (#1183) * fix(importer): watch band size range and list before the band key size-less A band size range written with " - " or an en dash between two bare sizes ("Sport Loop 42mm - 46mm"), and two or more sizes listed right before a band word ("GPS 40mm, 44mm Dây Sport Band"), name the band's sizes, not the case. Both now key size-less. A dashed size with more text after it, a single size before the band and a part that names the case keep their key. Closes #1162 * fix(importer): a band-led size or a case word before the list keeps the watch case size * fix(importer): watch range follows the last listed size; a band naming the case keeps it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rt and api v1 (#1182) * fix(web): apply category spec templates to compare, leaderboard, export and api v1 /compare, the leaderboard rows, the spec export, the key spec line and the REST API v1 product, list and compare responses now read the category's spec view from applyCategorySpecTemplate, like the product page (#1142). An earbud shows no screen, RAM, ROM or camera there and a watch no 5G row. Display mode of applyCategorySpecTemplate also drops the 5G parts of a watch's free-text network fields ("LTE / 5G" becomes "LTE"); storage mode keeps them. Closes #1166 * fix(web): strip 5g qualifiers and use template headings on compare Review follow-up for #1166: the 5G stripping removes bracketed 5G notes and qualifiers such as "SA/NSA/Sub6", also cleans sim_type, and compare group headings come from specGroupHeading so a watch has no "5G" heading. An export row without a field entry stays visible, and the JSON export schema label no longer claims ten groups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…shed (#1190) * fix(importer): kept variant keeps its own item and the left item is refreshed With RATE_ITEM_MODEL on, a kept stored variant (manual, or title/sku/ single_size evidence on a sizeless crawl) now takes item_id from that variant instead of the Item resolved from the title, and the Item a listing leaves gets refresh_item_stats like the variant it leaves. The stored listing is read in the hinted path too, for the refresh only; a caller hint is never overridden. A matcher without a mode reads as off. Flag off: unchanged. Refs #1185 * fix(importer): clear a stale item_id when the kept variant has no item Review follow-up: write item_id null explicitly in that case, and test the stored-listing and kept-variant read failures. Refs #1185 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mplates (#1191) * fix(web): api v1 filters and extension lookup follow category spec templates GET /api/v1/rate/products answers 400 for a spec filter the category's template does not have (has5g on smartwatches; ramGb, storageGb, compactScreen, minRefreshHz, hasNfc on earbuds), so a filter never returns a product whose scoped body contradicts it. The extension lookup returns the category's spec view and no storage tier for earbuds. Behaviour tests run the list, detail and compare routes and the lookup against a stubbed network; the regex-only API v1 checks are gone. Refs #1184 * fix(web): reject cross-category spec filters on category=all `all` fell back to the phone template, so `category=all&has5g=1` still returned watches whose body says `has_5g: false`. On `all` a spec filter must now apply to every category. The extension lookup no longer flags an earbud listing as an ambiguous storage variant. Tests check the filters reach search_master_products and that phone bodies stay unchanged. Refs #1184 * fix(web): keep category=all spec filters to categories that have them Per PO acceptance, `category=all` no longer answers 400 for a spec filter some category lacks. It keeps only products whose own category has every requested filter (`has5g=1` returns 5G phones, tablets and laptops, never watches or earbuds), reading the catalogue in 500-row pages and paging after the cut so offset and limit stay exact. Refs #1184 * fix(web): bound the category=all spec filter scan to 40 pages Review of the previous commit: a deep offset could read up to 100k rows page by page, and the whole table per page on the RPC fallback. 20k rows is well above the catalogue. Adds an `all&nfc=1` case. Refs #1184 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(web): hide empty spec rows and groups on the product page The product page printed spec rows with no value as "-": a GPS watch's data speed, 4G bands and VoLTE, an earbud's FCC approval, and an earbud hardware card with a single row. The rows now come from buildProductSpecGroups, which leaves out rows with no value and groups with no row; group numbers and the badge count follow the shown groups. Compare leaves out a row that every column has empty, so "-" stays only beside a value. Refs #1186 * fix(web): compare hides rows by the localized cell text Review of the previous commit: compare checked the raw value, so a row whose every cell localizes "N/A" to "-" stayed. The JSON export now skips all-empty rows like the CSV export. Refs #1186 * test(web): e2e expects hidden empty spec groups The seeded earbud has values only for audio, battery, Bluetooth and the sale status, and the seeded phone read as a watch has no network value once 5G is dropped, so #1186 now hides their empty groups. The 4G-only bands label moves to product-spec-rows.test.ts. Refs #1186 * fix(web): no spec toggle without hidden groups; e2e measures the hidden part Hiding empty groups (#1186) left the phone toggle on products with three groups or fewer, where it opens nothing; PhoneCollapsible now takes `collapsible` and the spec section passes it only past three groups. The collapse e2e asserted collapsed < 60% of expanded, which held for ten full groups but not for the sparse seeded phone, now five short groups. It now checks that collapsing removes the height of the groups it hides. Refs #1186 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…12gb (#1198) * test(normalizer): pin the thinkpad 21kc008pvn title to unknown, not 512gb The 2026-10-25 "Storage: 512GB" for CellphoneS SKU 21KC008PVN (a 1TB config) came from the registry default storage, removed by #1094 (#1101) on 2026-10-27. The current normalizer keys the storage-less title as :unknown and the title with its config as :1024gb. Refs #1189 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(web): show unknown specs as empty instead of invented values Compare printed made-up facts for unknown specs: "4G LTE" when 5G is unknown, stereo speakers, "USB Type-C", an official-sale status and a Vietnam MIC certification. The product page printed "No" for an unknown 5G flag and a default commercial status. Unknown is now empty, so the copy is removed from the locales and the spec dictionary. Refs #1193 * fix(web): read text 5g flags and cover known spec values Review of the previous commit: requiring a strict boolean hid 5G for older rows that store "true" or "yes" as text. known5g reads boolean or text flags, nested spec first, and treats anything else as unknown. Tests cover nested-over-flat precedence, text flags, a hidden all-unknown compare row and known availability and certification values. Refs #1193 * test(web): e2e earbud page has no invented sale status Without the default commercial status (#1193) the seeded earbud keeps three spec groups: audio, battery and Bluetooth. Refs #1193 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(web): no half-translated spec values in vietnamese localizeSpecValue replaced known English phrases inside longer values and left the rest in English, as in "không dây fast charging". A value the phrase map cannot cover now stays whole in English; one it covers is translated whole. Values already stored partly in Vietnamese still get their known phrases. The map gains pixels, performance/efficiency cores and photo/panorama, which covers every resolution, CPU and flash value in the catalogue. Refs #1187 * fix(web): check for untranslated words before the month rewrite Review of the previous commit: the month was translated before the whole-English check, so "release September 2024" came out half translated. The check now runs on the raw value. ĩ and ũ mark a value stored in Vietnamese; cd, in and oz are units; "ppi density" and "dual-band" are translated, which covers the usual resolution and Wi-Fi strings. Refs #1187 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ter (#1202) * fix(importer): tier 2 never links a storage-less title to a sized master With RATE_ITEM_MODEL off, match_master_product drops its storage filter for a NULL size, so "Laptop ASUS Zenbook S 14 UX5406AA-SU348W" scored above the accept threshold against the 1TB master. A Tier 2 hit whose model_key ends in a storage size is now skipped for a title with no storage, and the seed becomes a draft (#1105 option (a)). A ':unknown' master still takes the title; earbuds keep their legacy ':64gb' seeds. Flag-on matching is unchanged. Refs #1200 * fix(importer): watches keep their legacy sized seeds in tier 2 Review follow-up: "Apple Watch Series 10 42mm" has no only_storage_gb benchmark, so its title states no storage while the seed is keyed ':64gb'. Watches, like earbuds, are sold in one storage per model. Refs #1200 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1203) * fix(normalizer): read apple generation words, chips and part numbers "thế hệ 3", "Gen 3", "3rd Gen" and Roman numerals (II-IV) after a line read as the generation number. A chip (Apple TV 4K A15/A12/A10X, AirPods H2, HomePod S7) or a part number (MN873, MTJV3, MU8F2, ...) fills the generation only when the title states none. Apple Pencil is its own line instead of the iPhone fallback, also before "cho iPad". Existing fixtures keep their keys; across 769 Apple strings in the repo only two Pencil description lines change key. Refs #1194 * fix(normalizer): apple generation review fixes - "(thế hệ thứ N)" and "(Gen N)" in parentheses read as the generation. - Only "Apple Pencil" is the Pencil line; a stylus saying "Pencil" is not. - H2 no longer names AirPods Pro 2: the Pro 3 has it too. - Chips and part numbers are read only from the product's own part of the title, before a "+" bundle or a "cho/tương thích" phrase. - A part number without "/A" ("MN873SA") is read too. Refs #1194 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1204) * feat(web): sale state for published models with no in-stock listing A published model with no in-stock listing showed "0 ₫" and "0 sources" on its card and no reason on its page. masterSaleState derives on_sale, sold_out, discontinued, coming_soon, not_listed or no_stock from data the site already has: the in-stock price, the linked listings (product page), the launch status in the specs and an optional curated metadata.vn_sale_state. Cards and the product page show the state, and API v1 list, detail and compare bodies carry sale_state. No data write. Refs #1188 * fix(web): sale state never says sold out beside an in-stock offer Review of the previous commit: the stored price counts crawled, priced in-stock listings only, so a model whose only in-stock listing is extension-made or unpriced read "sold out" above that offer. The listing count is now unknown while any listing is in stock. The detail API uses the same count as the page, OpenAPI documents sale_state on detail and compare, and free-text "Announced ..." no longer reads as coming soon. Refs #1188 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1205) * fix(normalizer): a supported app or service sets no brand or model "Smart Tivi Samsung ... hỗ trợ Apple TV" keyed apple:tv because the brand chain read "apple" first. Mentions of an Apple app or service after "hỗ trợ / tích hợp / ứng dụng / kết nối / tương thích / supports / works with" are dropped before brand and model detection, and in a television title ("tivi", "smart tv") any Apple TV / AirPlay / HomeKit mention is. An iMac with a bundled Magic Keyboard already keys as the iMac; fixtures pin it. Refs #1195 * fix(normalizer): service mention review fixes - "Apple TV+" drops with its "+", so "iPhone 16 hỗ trợ Apple TV+" stays the iPhone 16, not a Plus. - A television title drops every Apple TV mention only when it names another TV brand: "Apple TV 4K - Smart TV Box" stays the Apple TV. - A colon after the lead word ("hỗ trợ: AirPlay 2") is read too. Refs #1195 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(importer): an unknown 5g is stored as null, not false The enricher wrote `has_5g ?? false` in its three return paths, and the chain before and after it guessed too: the normalizer set every non-registry phone to 5G and every tablet without "5G" in the title to no 5G, and the master seed and enrichMasterProductById coerced an unknown value to false. A phone or tablet title that names no 5G now gives has_5g null (and no invented cellular block); the enricher, the seed and the master enrichment keep null. Known values, registry entries and the no-cellular categories (laptops, watches, earbuds) are unchanged. Existing rows are not rewritten. Refs #1196 * fix(importer): read "5 G" titles and let the master schema hold an unknown 5g Review follow-up: "Galaxy A26 128GB 5 G" read as unknown (it was true before), and MasterProductSchema rejected has_5g null. An omitted value still defaults to false, like the column. Refs #1196 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ge (#1206) * fix(importer): ignore masterIdHint when the page states another storage ingestProduct trusted masterIdHint even when the page stated a different storage than the hinted master, so a 1TB page hinted to the 512GB master joined it. When the title states a storage and the hinted master has another, the hint is not applied: the listing is matched normally and ingest.master_hint_storage_conflict is logged. A page with no stated storage, or storage derived from the catalogue, keeps the hint. Refs #1197 * fix(importer): narrow the storage conflict and keep the hint's item Review of the previous commit (subagent and Codex): - a title listing several sizes ("256/512GB") named the hinted size but was read as a conflict; titleNamesStorage now keeps the hint then; - a title too bare to key would have failed the whole ingest, so it keeps the hint as before; - in item mode the conflict matched from the title alone; the hint's Item is now passed as the item hint; - in off mode the master the listing left kept stale market stats; it is refreshed with the new one. Refs #1197 * fix(importer): read whole size lists in titles, not ram or model digits Codex re-review: titleNamesStorage missed "128/256/512GB", "1/2TB" and "1.5/2TB" and took "64GB RAM" or "Spark64/128GB" for storage. titleStorageSizesGb now reads each slash list with its trailing unit, converts TB, and skips RAM and digits glued to a model name. The item-mode test uses a title keyed apart from the fixture, so it fails without the item hint. Refs #1197 * fix(importer): scan title sizes token by token Codex re-review of the regex: nested optional spacing in the repeated list group backtracked exponentially on long slash lists; "SSD512/1024GB" and "ROM128/256GB" lost their first size; and "SSD 512GB RAM 16GB" lost 512GB to a RAM lookahead. titleStorageSizesGb now folds spacing, splits the title into tokens and reads each size token with a separator-led, linear pattern. SSD/ROM/HDD prefixes may be glued, RAM labels bind to the size after them when the title writes labels first and to the size before them otherwise, and the scan reads at most 500 characters. Refs #1197 * fix(importer): bind ram labels per size and split unit suffixes Codex re-review: one title-wide label direction read "SSD 1TB 64GB RAM" as storage 64GB, and "256/512GB-VN/A" lost its list to the dash suffix. A RAM label now binds to a neighbouring size unless another size sits on its far side, where the title's label habit decides; a unit followed by - . : or ! ends its token; whitespace is collapsed first so every pass is linear. Refs #1197 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ants (#1208) * feat(importer): a new variant of a known item inherits its specs and publish state Flag-on only (RATE_ITEM_MODEL item / nullable_variant). When the matcher creates a missing variant under an Item that already has specs, the variant takes the Item's specifications, key specs, description and images, keeps only its own storage and RAM, skips the AI enrichment, and is published when the Item is published, the size is stated and the listing is a new device (design doc 4.1, 4.5 step 2). An Item with no specs yet seeds the variant as before. Codex P1 left open on #1151. Refs #1152 * feat(importer): seeds grouped by item, item specs from the seeds, series 10 storage - flagshipSeedsByItem() groups the flat seed list into Items with their variants; Item specs are the first variant's without its storage. - syncItemSeedSpecs() writes those specs onto existing rate.items rows, dry-run unless apply; runMasterSpecsBackfill calls it only with RATE_ITEM_MODEL on, and never creates Items (M1/M2 own that). - Registry: apple:watch-series-10 with only_storage_gb 64 (M0 inventory), so a Series 10 title without storage keys like its ':64gb' seed. 47 Series 10 fixtures move from ':unknown' to ':64gb'. Refs #1152 * feat(importer): spec enrichment per item with the flag on With RATE_ITEM_MODEL on, the matcher asks the AI enricher per Item: the prompt and cache key carry no storage or RAM, so every size of an Item shares one call, and the variant's own storage, RAM and storage key spec are laid over the answer (design doc 5.3). Mode off asks per size as before. Refs #1152 * test(importer): use the chip key spec in the per-item enrichment test Refs #1152 * feat(importer): a listing with no variant refreshes its item rating persistAnalysisToSupabase takes the listing's itemId. With no variant (cluster_id NULL, RATE_ITEM_MODEL nullable_variant) it calls the M2 refresh_item_stats RPC, which pools the rating over every listing of the Item (design doc 4.1, Q4). With a variant, refresh_master_product_rating runs as before (its M2 body resolves the Item). No itemId, no change. Refs #1152 * fix(importer): item review fixes for status, sizes, flag guards and seeds - A new variant under a draft or archived Item is a draft, with or without Item specs; an inherited variant also honours needs_review. - Inherited variants take the flat screen, battery and 5G columns from the Item specs, and their own RAM and RAM key spec. - Per-Item enrichment never takes storage or RAM from the AI answer, even when the variant's own is unknown. - syncItemSeedSpecs and the analyzer's Item rating refresh are no-ops with RATE_ITEM_MODEL off; Item specs never keep storage or RAM, also when an existing Item row has them; seed Item names drop only a trailing storage and seed Items carry no RAM. - Registry `case_mm`: the Series 10 entry describes the 42mm case, so a 46mm title gets none of its size specs and is not benchmark-enriched. - The pipeline passes itemId to persistAnalysisToSupabase. Refs #1152 * fix(importer): series 10 46mm resolution and the pipeline's item mode for ratings Codex re-check follow-up: a 46mm title no longer takes the 42mm resolution, and persistAnalysisToSupabase takes the pipeline's own itemModel (the env is only the default), so a pipeline configured with the Item model on refreshes Item ratings. Refs #1152 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1221) * fix(normalizer): pin the series 11 registry entry to its 46mm case The apple:watch-series-11 entry describes the 46mm case, but every Series 11 title used its screen, battery, weight, dimensions and resolution and was benchmark-enriched. With case_mm 46 (added for Series 10 in #1152), a 42mm title gets none of those and is not benchmark-enriched, so its Tier 3 master is a draft. Keys do not change. Closes #1211 * test(normalizer): series 11 size-less title and 42mm dimensions Refs #1211 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1220) * fix(web): ai review summary says it covers one store's latest analysis The summary sentence comes from the latest review_analysis row, which is one listing's batch, while the page and the percentages beside it count every review. "Đã phân tích 5 đánh giá" therefore read as 5 of the 15 shown. The vi/en/cs copy now says "Lần phân tích gần nhất (một nơi bán) xét 5 đánh giá" and drops the batch's own rating, which is not the product score. Scoring is unchanged. Refs #1216 * fix(web): read the summary as 'đã xét' in vietnamese Refs #1216 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1226) * fix(web): four vietnamese copy defects on product, compare and deals - sensor-shift OIS reads "chống rung dịch chuyển cảm biến (OIS)" once; the bare OIS rule no longer rewrites it again (#1210). - the product quick-spec durability uses localizeSpecValue like the spec section (#1219). - compare skips the ppi suffix when the resolution already names it; a bare "No" storage expansion is spelled "No card slot" so it localizes, and vi reads "Không hỗ trợ thẻ nhớ" like the en/cs maps (#1214). - deals count reads as a display limit, not as devices failing the budget (#1218). * fix(web): skip the compare ppi suffix for unspaced ppi too Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… apart (#1224) * fix(web): review cards label the rating weight and the trust estimate apart Every review showed "Trọng số: 1.00x" on its badge and "Trọng số 0.50x" in the line under it. The badge is the weight the product score uses (helpfulness_weight, after votes and reports); the line is the 6-factor trust model's estimate. The badge now reads "Trọng số tính điểm", and the model's explanations start with "Độ tin cậy" (en "Rating weight" / "Trust", cs "Váha v hodnocení" / "Důvěryhodnost"). Scoring is unchanged. Refs #1215 * fix(web): call the trust model's figure an estimate, label merchant weights alike Review: "Kiểm tra độ tin cậy (6 yếu tố): Độ tin cậy 0.50x" repeated the label, and the figure (A·B·C·D·E·F) takes in depth and votes, not trust alone. It now reads "Ước tính 0.50x" (en "Estimate", cs "Odhad"). The merchant page's review flag names the same helpfulness weight "trọng số tính điểm" like the review badge. Refs #1215 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(normalizer): key garmin forerunner 265s apart from the 265
"Garmin Forerunner 265S 42mm" matched /forerunner\s*265/ and keyed
garmin:forerunner-265:8gb with the 46mm 265's registry specs. The 265S
("265S", "265 S", "265s") is now its own model, garmin:forerunner-265s,
with no registry entry (storage unknown, not benchmark-enriched). The
265 keys as before.
Closes #1222
* fix(normalizer): read 265s glued or hyphenated, not a s/m band size
Codex follow-up: "265S42mm" and "265-S 42mm" are the 265S;
"265 S/M band" and "265 S-M band" are the 265 with a band size.
Refs #1222
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ch (#1229) * fix(importer,web): ai review summary says it covers one listing's batch The analyzer stored "Đã phân tích N đánh giá ... (điểm JReviews có trọng số x/5★)" as if N were the product's reviews and x its score. It now stores the #1216 wording ("Lần phân tích gần nhất (một nơi bán) đã xét ...") without the batch score. The review summary reader rewords old rows the same way through listingReviewSummary, so API v1 /products/{slug}, /reviews and the extension side panel print the honest sentence; the web page localizes both stored forms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ore it (#1236) * fix(normalizer): a size that names the case wins over a band size before it "Apple Watch Series 11 GPS + Cellular 45mm Sport Band, 46mm Titanium Case" keyed -45mm: the band size came first outside any band phrase. A title part that names the case ("Case", "vỏ", "mặt") with exactly one size and no band word now gives the case size in either order. None of the 173 watch strings in the repo change key; the #1124/#1137/#1162 fixtures pass unchanged. Closes #1223 * fix(normalizer): a named case size must sit next to its case word Review follow-up: the size must be next to "case", "vỏ", "viền" or "mặt" (up to two words between), "viền" counts, and a part about a box, a cover or a fit ("kèm vỏ hộp 42mm", "fits 42mm case") names no case. Refs #1223 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(web): a 1v1 tie says tie and score gaps read as points
The versus page named a leader on a 5.00 vs 5.00 tie ("dẫn trước",
"Dẫn đầu 1v1 … +0.00"), and the featured pairs wrote the score gap as
"Chênh 0.00 đ", which reads as đồng. isVersusScoreTie compares scores at
the shown two decimals; on a tie the verdict, the banner, the Markdown
copy and the JSON export say the two are level (winner null), and the
gap badge reads "Chênh {delta} điểm".
Refs #1209
* fix(web): ties in the versus csv, api and compare pairs
Review of the previous commit: the CSV export still named a winner on a
tie; API v1 /versus had no tie flag (is_tie added, overall_winner kept as
the tie-break); the compare featured pairs showed "Chênh 0.00 điểm" where
the home page says tie; the Markdown tie line kept the "leading device"
label; vi said "Cân bằng" in one place and "Ngang điểm" in another. The
tie test now checks the vi copy that had the "đ".
Refs #1209
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* perf(web): reuse cached master reads in merchant profiles A cold merchant profile waited on one database round trip after another and re-read every listed master's offers and price history uncached. It now reads each master through the product pages' cached readers, reads the listings while the merchant row loads, and runs the master analysis alongside the master, category and review reads. * fix(web): add merchant discount analysis outside the profile cache A nested unstable_cache skips its own cache read in Next 16, so the cached master readers called inside the profile entry re-read every master. The cached entry now holds the profile without its discount analysis and the listing-to-master map; the analysis is added per request from the product pages' cached offers and price history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onest (#1239) * fix(web): privacy policy covers the extension; ingest limit copy is honest - /privacy-policy gains a section on the browser extension in vi, en and cs: what lookup sends without opt-in, what the opt-in adds, what stays in the browser, and that typed searches are sent (#1233). - The ingest route comment, 429 message and OpenAPI text say the 15/min per-IP limit is best effort per server instance and that the RPC cap of 120 crowd jobs/min is the global bound (#1234). A real global per-IP limit needs a shared store or DB function (DOS-Me migration). * fix(web): the ingest 429 comes only from the per-instance limit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…hosts (#1241) * fix(extension): truthful opt-in label, store doc and narrower tiktok hosts - The opt-in label lists the ten fields the crowd signal sends; the payload is built by buildCrowdIngestPayload next to the labels, and a test ties the label to the payload keys (#1230). - CHROMEWEBSTORE.md justifies exactly the manifest's permissions and hosts (no tabs or localhost; adds TikTok, Hoang Ha, Di Dong Viet, Phong Vu, www.rate.vn) and describes what lookup and the opt-in send; a test compares the doc with the manifest (#1231). - TikTok is limited to www.tiktok.com and shop.tiktok.com, the hosts its product pages use (#1232). What the extension collects is unchanged. * docs(extension): store doc lists the session cache and side panel searches * docs(extension): the session cache also holds the latest search Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m-aware (#1243) * feat(importer): make arena spec sync, review risk and peer floors item-aware Behind RATE_ITEM_MODEL (#1213, design doc 4.1, 4.4, 5.3): - syncArenaSpecsToItem writes Arena specs to rate.items without storage or RAM, and fills only empty storage/RAM overrides on sized variants; dry-run by default, never creates Items, no-op with the flag off. - the Arena audit counts a flagship present when its Item is (another size or a rate.items row) and lists Items with incomplete specs. - resolveClusterPeerFloorPrice takes an optional variant id: a listing with no variant gets no floor and NULL-variant rows never enter one; seller review risk passes it. Flag-off change: the Arena parser stores an unknown 5G as null and invents no cellular defaults (#1196). * fix(importer): arena item sync keeps one ram and writes only unchanged rows Review findings on #1213: - writes are a compare-and-swap on updated_at (variants also not archived); a row changed after the read is counted as a conflict; - a stored RAM number or label wins over Arena, so the two never disagree; - slash RAM options (8GB/12GB RAM) are RAM, never storage, and name no single RAM; memory variants come from the GSMArena line only; - the audit keeps the raw item_key of an Item it cannot sanitize. * fix(importer): a suffixed ram label wins over the arena ram Codex round 2 on #1213: a stored key_specs.ram such as '8GB LPDDR5X' was ignored, so the sync wrote Arena's ram_gb next to a different label. The label's leading size is now read, and a label that names no size blocks Arena's RAM. * test(importer): build the upper-case arena key at run time The shared-data key scan reads every three-part literal in the importer test files and requires it to parse, so the upper-case fixture failed the monorepo build (#1213). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… not products (#1255) * fix(extension,web): tiktok pdp item ids; brand and category pages are not products - TikTok Shop /shop/vn/pdp/<slug>/<id> (and /shop/pdp/...) yields <id> in the extension extractor and the server URL parser (#1245). - On slug-id stores a path ending in a bare brand (mobile/apple.html, dien-thoai/samsung) or "-series" is a listing, and CellphoneS paths below the root are categories, so none yields an item id; product slugs are unchanged (#1246). * fix(web): the server url parser skips the same listing pages as the extension Mirror the brand, "-series" and CellphoneS sub-path rules in parseEcommerceUrlOrQuery so a pasted category link or an older client's ingest no longer resolves to item id "apple". Accessory slugs ending in "-series" (op-lung-...-iphone-16-series) stay products in both (#1246). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
) * fix(web): keep merchant listing prices with their discount verdicts The listing price now comes from the same cached offers the verdict was computed on, with the store's min and max price recomputed from them. A discarded speculative listings read no longer logs an error, and the merchant profile tests cover offer pass-through, one read per master, unclustered listings and the analysis staying outside the cache entry. * fix(web): show the price a merchant listing's verdict was computed on Taking the price from the cached offers could still disagree with a verdict from an older price-history entry, and an offer price of 0 could replace a valid one. A listing with a verdict now shows the verdict's own price, always valid, and the listings stay sorted by the price shown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(web): versus verdict follows the scoreboard's weights
The verdict under the versus title was rendered on the server from the
default weights, while the scoreboard below re-scores with the persona
the reader picks, so the two could name different leaders. The verdict
now lives in the scoreboard and reads its result through
versusVerdictCopy: it names the same leader or tie as the banner and
says which weights it uses when they are not the default ones ("(Theo
trọng số: Chơi game nặng.)"). The default view reads as before.
Refs #1235
* fix(web): announce the versus verdict and test the component's mapping
Review: a persona click changed the verdict silently for screen readers,
and the test rebuilt the component's score and label mapping instead of
testing it. versusVerdictFor now holds that mapping (leader's score, tie
score, persona label with the custom fallback) and the component calls
it; the verdict is aria-live polite. The test also checks the flipped
score and the fallback label.
Refs #1235
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(web): watchlist asks for a size when an entry is an item Price alerts stay per size. With RATE_ITEM_MODEL on, an entry the watchlist API marks `size_required`, or a save answered 422 SIZE_REQUIRED, shows the Item's sizes from the product API; choosing one replaces the entry with that size, its price and a target 5% below. With the flag off the API never sends either, so nothing shows. * fix(web): size picker keeps a watched size and moves the cloud alert Review of the previous commit: choosing a size already on the watchlist drops the Item entry and keeps that size's own target; the chosen size's cloud alert is saved when signed in; when the sizes cannot be read the entry says so and links to its product page; the size group is labelled by its title; sync-all names SIZE_REQUIRED in the page's language. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…erdicts (#1287) * fix(web): item pages live under their own category; unknown-size history Item page follow-ups of #1154 (#1281), behind RATE_ITEM_MODEL: - the route category is checked against the Item's category_id: an Item URL under another category, and an old variant URL redirected from one, 308 to the Item's own category; an Item whose category is no catalogue category is not found; an Item with no category keeps the requested one; - the "storage not stated" group gets its listings' price-history verdicts, cached per Item and per their current prices. Refs #1281 * fix(web): read an uncached item category, canonical of a redirected url Codex round 1 on #1281: - a category id missing from the cached map is read by id before the Item is called lost, so a new category is never a 404; - metadata of a URL the page 308s away points its canonical at the redirect target, not the requested category; - the unknown-size history cache key hashes the listings' prices. Refs #1281 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(proposals): item model go-live runbook with vietnamese version Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(web): item catalogue, versus and sitemap edge cases Part 2 of the #1155 follow-ups, behind RATE_ITEM_MODEL: when the Item links fail, the catalogue page is cut from the variants it already read instead of reading them again; a master whose Item is not published compares as a plain master, as with the flag off; an Item slug holding "-vs-" keeps its variant slug as a versus side; the sitemap's versus pairs use the same variant-to-Item map as its product URLs. * fix(web): no versus page for a slug holding -vs- Review of the previous commit: a side whose URL slug holds "-vs-" (Item or variant) cannot parse back, so it is a 404 instead of a redirect to an unparsable URL, and the sitemap skips such pairs; a failing Item links read is logged as an error, a missing schema as a warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
) * fix(web): item versus links, filtered size chips and the read cap Behind RATE_ITEM_MODEL: the compare page's 1v1 link, the matchup switcher and the scoreboard swap link name Item pairs (no 308 hop), from a slug map the pages pass only while the flag is on; with a ?storage= filter a card's chips list only the matching sizes, as its price does; a category too large to group in 20 read batches shows its variant listing instead of a page cut short or empty. * test(web): item slug map for in-app versus links Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(web): restored compare tray links item pairs without a 308 With RATE_ITEM_MODEL on, the compare page's 1v1 link also names the Item pair for products the client added or restored from the tray: their Item slug is read from the product API once per slug. With the flag off the page passes no slug map and nothing is read. * fix(web): the compare page's slug map covers slugs with no item A slug the server resolved but found no Item for maps to itself, so the compare workspace does not read it again on the client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…1296) * fix(web): item pages live under their own category; unknown-size history Item page follow-ups of #1154 (#1281), behind RATE_ITEM_MODEL: - the route category is checked against the Item's category_id: an Item URL under another category, and an old variant URL redirected from one, 308 to the Item's own category; an Item whose category is no catalogue category is not found; an Item with no category keeps the requested one; - the "storage not stated" group gets its listings' price-history verdicts, cached per Item and per their current prices. Refs #1281 * fix(web): read an uncached item category, canonical of a redirected url Codex round 1 on #1281: - a category id missing from the cached map is read by id before the Item is called lost, so a new category is never a 404; - metadata of a URL the page 308s away points its canonical at the redirect target, not the requested category; - the unknown-size history cache key hashes the listings' prices. Refs #1281 * feat(web): load item size panels on demand for items with many sizes #1285, behind RATE_ITEM_MODEL. An Item page rendered every size's price strip, history and offer table into the RSC payload, about 30 KB per size (#1281). Above 4 sizes, or when the other sizes' offers and history pass about 150 KB, the page now renders only the default size; another size loads when picked from a cached route, /api/v1/rate/item-size/{variant slug} (revalidate 300), and shows with the same client components (price strip, history, offers), keyed per size. Smaller Items render as before; the page stays ISR. - e2e: a six-size Item in the item-model seed; the spec logs the RSC bytes of the lazy and an eager Item and checks a picked size loads. - client messages: the saleState labels and two loading strings. * fix(web): size route only for a published item's sizes, fresh data Codex round 1 on #1285: - the size route answers only a published size of a published Item (not a draft Item, a retired size or a master with no Item); - the browser shares only the request in flight per size, so a later pick reads fresh data instead of a page-lifetime cache; - a failed size has a retry button; - the 150 KB trigger counts UTF-8 bytes. Refs #1285 * Merge remote-tracking branch 'origin/dev' into feat/1285-lazy-item-size-panels # Conflicts: # apps/web/src/server/rate/item-route.ts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eader misses (#1292) * fix(normalizer): a gift size never stands in for a product size the reader misses * fix(normalizer): bare-size guards for warranty, percent, ram and os; worded watch size outside bands; comma gift lists * fix(normalizer): instalment counts, gifted worded sizes and size lists are no watch or screen size Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(web): compare links with ?cat= open on their category Links across the site and the extension write `/compare?cat=...` while the compare page read only `category`, so they opened on smartphones. The page now reads both; `category` wins when both are set. * fix(web): an empty compare category parameter is no value Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
) * fix(extension): price list, badge, empty and error states, labels The side panel orders in-stock offers first and marks sold-out, discontinued and bait rows (the lookup now flags bait offers), shows a short state for not-found and accessory pages, an error card with retry for failed lookups, and no 0.0 stars or 100% genuine without reviews. The badge and pill warn on a bait price and never say OK for an unchosen size or an accessory; labels are Vietnamese and the panel is labelled for screen readers. * fix(extension): keep the crowd signal and ignore late lookup answers Review of the previous commit: the opt-in crowd signal is sent whatever the lookup answered, as before (#1175); only the tab's latest lookup may set or clear its pill, badge and panel, so a late failure for an earlier product no longer clears the current one; an open side panel reloads when the page's lookup fails; a bait price shows the amber verdict style in the panel too. * fix(extension): no crowd signal after a network error, as before A network error used to throw before the opt-in crowd signal; the lookup now reports it as a status-less failure, so the signal is skipped there to keep what the extension sends unchanged (#1175). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
) * fix(web): a post-login target that normalizes to //host goes home The callbackUrl sanitizer checked the raw value for a leading '//', but dot segments normalize into a protocol-relative path after parsing, which the router reads as another host. The parsed pathname is now checked too. The locale-prefix 308 also collapses leading slashes, so it never emits a two-slash Location. Closes #1305 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…changes (#1320) * fix(web): a vote flushes only its product's caches, and only when it changes #1306 (web part). Every vote request revalidated the global products, stats and reviews tags, and the database limit counts rows per review, so repeated actions on one review could keep the catalogue caches cold. - the stored vote is read first: the same vote again is not written and flushes nothing; - a changed vote revalidates the review caches and its product's page tag, not the global products and stats tags; - flushes are throttled per voter (6 per minute per instance); votes past that still count and the caches catch up on their own interval. The database guard change is proposed on the issue for JOY. Refs #1306 * test(web): vote route revalidates its product tag, not the global ones Refs #1306 * fix(web): a vote refreshes only its product's review caches Codex round 1 on #1306: - review readers and summaries are also tagged per master (masterReviewsCacheTag); a vote revalidates that tag and the product's page tag, no longer the global reviews tag; - a report over a stored up/down vote counts as a change (the report clears the vote), so it is written. Refs #1306 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(web): reviews by product id serve published products only * fix(web): merchant profile reads no reviews of unpublished products * fix(web): platform stats sample no reviews of unpublished products Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(web): rate-limit inspect and lookup and cache their results #1307. /api/v1/rate/inspect and /lookup ran several database reads per distinct query with no rate limit, and their result was cached for one request only. - both routes share a per-IP limit of 30 per minute per instance (429 with Retry-After); a durable shared limit is #1247; - the inspection result is cached in the Data Cache for 2 minutes per normalized query (hashed key), also for the inspect page; - the client key prefers the platform-set x-vercel-forwarded-for over a client-supplied forwarded chain (also for the extension routes). Closes #1307 * fix(web): trust the vercel ip header only on vercel; skip oversized cache entries Codex round 1 on #1307: - x-vercel-forwarded-for is read only when VERCEL=1; elsewhere it is client input and the forwarded chain is used as before; - an inspection result over 1.5 MB (Next skips entries over 2 MB) is cached as a marker and read uncached, instead of a failed write. Refs #1307 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s a public one (#1315) * fix(web): public api errors answer a fixed message unless the error is a public one Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ipt nonce (#1318) * feat(web): report-only content security policy with a per-request script nonce * fix(web): csp report endpoint caps streamed bodies, validates and dedupes before logging Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(extension): vi, en and cs through chrome.i18n Side panel, trust pill, toolbar badge and manifest text now come from src/_locales (default vi, copied to dist by the build) through a typed t() helper that falls back to the vi catalogue without chrome.i18n. Tests pin identical keys and placeholders and fail on Vietnamese left in src. Server text stays as the API returns it (#1304). * fix(extension): vietnamese description within chrome's 132 characters The Vietnamese manifest description was 145 characters, over Chrome's limit; it is shortened with the same meaning, the store listing's short summary uses the same text, and the drift test checks name and description lengths in every locale and the summary against the vi catalogue. * fix(extension): count wording that reads right for any number English and Czech messages with a count no longer need a plural form, and the Czech empty-review text says recenzí as the other labels do. * test(extension): expect the count wording for reviews Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1327) * test(web): lock the cache mode of the home page and the category pages Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.
Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
Promote dev to main.
Merge with a merge commit, never squash (Docs/Resources/RELEASE.md).