Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions .claude/commands/practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ raw = (frontOverall * 0.60) + (backOverall * 0.40)
overall = roundGrade(min(raw, lowestSubgrade + 1))
```

- Card detection uses Sonnet (reliable vision), subgrades use configured model
- Card detection uses Together AI (GLM-4.6V-Flash) when `TOGETHER_API_KEY` set, falls back to Claude Sonnet
- Subgrades use configured model (default Claude Sonnet)
- SSRF protection: `validateImageUrl()` in preprocessing.js — DNS resolution, private IP blocking, blocked hosts
- `gradeSubgrade` receives pre-built image blocks (not URLs) — use `imageBlockFromUrl()` or `imageBlockFromBase64()`
- `cropCorners()` accepts URL or Buffer
- Back-only subgrades skipped when no back image — front score substituted
Expand All @@ -73,7 +75,7 @@ overall = roundGrade(min(raw, lowestSubgrade + 1))

```javascript
if (req.query.demo === "true") {
// return canned data from lib/data/demo.js
// return canned data from lib/cards/demo.js
return res.json({ ...demoData, _demo: true });
}
// ... live data path
Expand All @@ -92,15 +94,15 @@ if (req.query.demo === "true") {

## Testing pattern

Unit tests (test/unit-test.js, ~172 tests):
Unit tests (test/unit-test.js, ~212 tests):
```javascript
test("descriptive name", () => {
eq(actualValue, expectedValue);
});
```
- Sync test harness, no async support (use dynamic import for modules needing env setup)
- Group with `console.log("\n\x1b[1m=== section ===\x1b[0m")`
- Sections: parseGradeJSON, buildEbaySearchQuery, detectLanguage, tokenizeQuery, extractPokemonName, normalizeListingLanguage, parseListingLanguagesFromInput, filterByLanguage, titleLooksGradedSlab, titleMatchesSlabListing, parseSellerSlabFromConditionText, filterByListingFormat, filterRelevantResults, querySeeksJapaneseMarket, filterToLikelyTcgCards, demo data, detectCondition, filterByCondition, flagPriceOutliers, parseCardIdentity, resolveCardIdToQuery, findDemoByNumber, demo multi-source + sold dates, cornerCropsToImageBlocks, demo image resolution, demo grade confidence, alert email, portfolio, portfolio history + gainers/losers, csvEscape + csvRow, isGradedCard, card database, price trend, JWT auth, findCardByCardId, roundGrade, image block helpers, subgrade prompt keys, computePriceTrend edge cases
- Sections: parseGradeJSON, buildEbaySearchQuery, detectLanguage, tokenizeQuery, extractPokemonName, normalizeListingLanguage, parseListingLanguagesFromInput, filterByLanguage, titleLooksGradedSlab, titleMatchesSlabListing, parseSellerSlabFromConditionText, filterByListingFormat, filterRelevantResults, querySeeksJapaneseMarket, filterToLikelyTcgCards, demo data, detectCondition, filterByCondition, flagPriceOutliers, parseCardIdentity, resolveCardIdToQuery, findDemoByNumber, demo multi-source + sold dates, cornerCropsToImageBlocks, demo image resolution, demo grade confidence, alert email, portfolio, portfolio history + gainers/losers, csvEscape + csvRow, isGradedCard, card database, price trend, JWT auth, findCardByCardId, roundGrade, image block helpers, subgrade prompt keys, computePriceTrend edge cases, API response parsing (mock), card detection bounds, validateAndShape, buildSignal, deriveEra, v3 formula edge cases

API tests (test/api-test.js, ~130 tests):
```javascript
Expand All @@ -111,6 +113,9 @@ await test("GET /api/endpoint returns expected", async () => {
```
- `json()` for auth'd requests, `jsonNoAuth()` for public
- Auth tests accept both success and 401 (local dev disables auth)
- Mock pattern: extract response parsers as pure exported functions, test with sample payloads (no framework needed)
- `parseAnthropicResponse(data)` / `parseTogetherResponse(data)` in preprocessing.js
- `validateAndShape(provider, mode, o, raw)`, `buildSignal({...})`, `deriveEra(setCode)` — exported for testing
- Sections: health, drops, webhooks, comps, search, sold, psa, grade, auth, admin keys, condition, card, arbitrage, price-history, track-prices, errors, demo data, portfolio, portfolio/history, portfolio/export, grading-opportunities, card/view, set browser, price trend, collection tracking, google oauth, upload url, developer self-serve, analytics, autocomplete, set detail, grading dataset, grade validation

## Naming conventions
Expand Down
184 changes: 29 additions & 155 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,166 +4,40 @@

## 1.3.0 (2026-05-15)

### Added
- AI grading v3: 8 subgrades (centering/corners/edges/surface x front/back) with 60/40 weighting
- Card boundary detection: Haiku preflight auto-crops card from background in user photos
- ML dataset pipeline: passive slab image collection from eBay sold listings (grading-dataset Firestore)
- GET /api/grading-dataset/stats: owner-only endpoint to monitor dataset collection
- SSRF protection: URL validation with DNS resolution, private IP blocking, blocked hosts
- Token usage + estimated cost tracking per grade
- Coding practices skill (.claude/commands/practices.md)
- 21 new unit tests (172 total), 13 new API tests (~130 total)

### Changed
- Overall grade formula: (front avg x 0.60) + (back avg x 0.40), capped at lowest subgrade + 1
- Grade response mode: "llm-detailed-v3" (was "llm-detailed")
- Corner crops now labeled per side (front/back), passed only to their respective subgrade
- gradeSubgrade accepts pre-built image blocks instead of URLs
- cropCorners accepts Buffer or URL
- AI grading v3: 8 subgrades (front/back), 60/40 weighting, centering ratios (lr/tb), tilt correction
- Card detection: 4-corner detection, auto-crop + straighten from user photos, SSRF protection
- Together AI provider for card detection (GLM-4.6V-Flash, ~90% cost reduction)
- ML dataset pipeline: passive slab image collection from eBay sold listings
- Demo data upgraded to v3 format with centering ratios
- lib/ restructured: auth/, cards/, data/ separated by concern
- 215 unit tests, ~130 API tests

## 1.2.0 (2026-05-15)

## 1.1.0 (2026-05-15)
- Google OAuth sign-in with JWT, developer self-serve API keys (create/rotate/revoke)
- Admin key management (CASECOMP_ADMIN_SUB), per-key rate limiting
- Request analytics endpoint (owner-only)

### Added
- Set browser: GET /api/sets (238 sets with logos, era groups, card count breakdown) and GET /api/sets/:setCode (cards with rarity)
- Collection tracking: GET /api/portfolio/set/:setCode returns owned cardIds for progress bars and owned/missing indicators
- Set metadata from TCGdex: 236 named, 154 with logos, official/secret card count split
- Rarity data from TCGdex rarity-filter endpoints (~4K cards tagged: SAR, IR, UR, HR, SR, AR, RR, ACE, CR)
- Era classification: Scarlet & Violet, Sword & Shield, Sun & Moon, XY, Pokemon TCG Pocket, Diamond & Pearl, Black & White, Classic
- Sigstore container signing: cosign keyless signing via GitHub OIDC, deploy by image digest
- SBOM generation: Syft SPDX JSON uploaded as build artifact (90 day retention)
- Grype vulnerability scanning: SARIF report uploaded to GitHub Security tab
- Binary Authorization: DRYRUN audit policy on both Cloud Run services
- CodeQL SAST: static analysis on PRs + weekly schedule
- Terraform CI: plan posted as PR comment, auto-apply on merge to main
- Card autocomplete: GET /api/autocomplete with TCGdex EN+JP database (29K cards), card preview images, EN→JP name mapping
- Search filters: format (raw/slab), multi-select source pills, condition dropdown, slab provider+grade selectors
- Autocomplete dropdown on dashboard: card thumbnails, card preview panel on hover, keyboard navigation
- Lazy PSA loading: search returns results without waiting for PSA, frontend fetches PSA separately
- Pre-warm cache: track-prices scheduler pre-caches active listings + PSA for tracked cards
- Fast card-first search: autocomplete → card share → demo search → render in 2-3s (was 30s)
- Client-side format filtering: Raw excludes slabs, Slab matches provider+grade
- Client-side condition filtering: instant without re-fetch
- Sort by grade (high to low) added to sort dropdown
- Pagination: 25 listings per page with "Show more" button
- Autocomplete suppressed on hint chip clicks
- Card-centric view: GET /api/card/view/:setCode/:number returns raw + graded data with PSA + grading ROI comparison
- Public sitemap: GET /api/sitemap returns all indexable URLs (static + card pages), supports ?format=xml for Google
- TCGdex card database cached in Firestore (24h TTL, instant startup, background refresh)
- PSA negative caching: "not found" results cached 7 days (no more DuckDuckGo retries for JP SARs)
- Search frequency tracking + pre-warm: top searched + portfolio cards pre-cached by scheduler
- eBay ship-to verification skipped (worldwide by default) — search 30s → 200ms
- eBay sold decoupled: returns active immediately, sold fires in background
- Seller feedback filtering: feedbackScore <5 or feedbackPercentage <90% removed
- Expanded blocklist: digital codes, mystery packs, playsets, grading supplies
- Price floor: listings below 20% of median flagged as suspicious
- Korean filter: auto-detected from JP set codes and card number denominators
- Auto-derived 186 set totals from TCGdex (248 total sets, was 30 hardcoded)
- cardId + cardIdentity added to search responses
- Leading zero handling in card number denominators (114/083 → m4)
- eBay relevance filtering: blocklist expanded (art case, sleeves, playmat, booster, etc.), applied to active+sold
- Arbitrage alerts: notify when cross-source spread exceeds threshold (POST /api/alerts with type "arbitrage")
- Price drop alerts: notify when price falls below target (POST /api/alerts with type "price")
- check-alerts endpoint (owner-only): evaluates all active alerts against live data
- Live price tracking: track-prices fetches real eBay sold + magi comps (was demo-only)
- Cloud Scheduler: track-prices + check-alerts run every 6 hours
- Grading ROI card: "Grade This Card?" panel with raw price, grading cost, total, gem rate, verdict
- Population-aware expected outcome: maps AI pre-grade to likely PSA grade with scarcity indicator
- TCGPlayer market price reference in price chart (with wrong-card sanity filter)
- Ungraded listing indicators: dash chip on cards + "AI grading unavailable" note in detail panel
- Playwright smoke test suite (40 tests): dashboard UI, detail panel, tabs, PSA stats, arbitrage, mobile viewport
- Sort dropdown on listing tabs (price ascending/descending)
- Result counts in tab labels: "Active (6)" / "Sold (3)"
- Condition badges on raw listing cards using detectedCondition from API
- Price outlier warnings (flagPriceOutliers applied in API pipeline)
- GRADED badge for slab listings in detail panel
- Inline PSA stats in Prices tab with gem progress bar
- Price chart x-axis date labels, redraws on tab switch (fixes blank canvas)
- Arbitrage "Best Price" chip and savings summary
- Fade-up entrance animations, sticky frosted header, sticky search bar
- Alert form: toggle between Price Drop and Arbitrage Spread types
- Developers nav link in dashboard header
- AI grading: corner crop preprocessing via sharp (8 magnified crops from front+back for corners subgrade)
- AI grading: all listing images passed to centering/edges/surface (corners uses front+back + crops only)
- eBay image resolution upgrade: s-l500 (500px) to s-l1600 (full resolution)
- Email notifications: Resend integration for price and arbitrage alerts with 6h dedup
- Portfolio tracker: Firestore CRUD, 5 API endpoints (GET/POST/DELETE/PATCH /api/portfolio + /api/portfolio/summary)
- Portfolio demo data: 3 cards (Umbreon, Greninja, Pikachu) with purchase prices and current values
- Portfolio dashboard UI section with stats grid and card list showing ROI
- Portfolio value history: GET /api/portfolio/history with daily snapshots, track-prices scheduler extension
- Portfolio gainers/losers: extended summary with top 3 gainers/losers by price change %
- Portfolio CSV export: GET /api/portfolio/export?format=csv with UTF-8 BOM, card identity enrichment
- Portfolio grading opportunities: GET /api/portfolio/grading-opportunities flags ungraded cards worth grading
## 1.1.0 (2026-05-15)

### Changed
- Dashboard UI synced with casecomp.xyz frontend: Inter Tight + JetBrains Mono fonts, pill-style tabs/hints, ghost view button
- Moved lib/demo.js to lib/data/, lib/output.js to lib/search/
- Umbreon demo data: now multi-source (eBay + magi + Yahoo) with detectedCondition NM/LP
- All demo sold data spans 30+ days with realistic date spreads
- Detail panel: prefer detectedCondition over "Ungraded"
- Consistent shipping display with green "Free shipping"
- CI: unit + smoke run in parallel, both required by branch protection
- TCGPlayer search: full query first, fallback to simplified, price sanity check
- Demo rate limit shown correctly as 360/min
- PR template: added breaking changes + demo data check sections
- Yahoo Auctions: relevance filtering applied (removes 1-yen box auctions, unrelated cards)
- Card identity: cleaned up long names (strips pack names, condition text from titles)
- track-prices: now also tracks cards from active alerts, not just 3 hardcoded defaults
- Demo condition filter: checks detectedCondition in addition to raw condition field
- Tests: 300 total (130 unit + 96 API + 74 smoke), up from 183
- PR template: type, endpoints, checks, infrastructure, frontend impact, deploy notes
- Kaniko pinned to v1.23.2 with --reproducible builds, dual tags (latest + SHA)
- Deploy by image digest instead of :latest tag
- Health endpoint: eBay usage stats hidden from non-owner requests
- PSA endpoint: demo mode returns canned data (was making live API calls)
- All error responses use safeErrorMessage() (fixed 3 places leaking raw e.message)
- AI grading prompts: full PSA rubric (5-10), perspective correction, per-corner/edge detail, holo-specific surface guidance
- Demo grades re-evaluated with improved prompts (more conservative scores, honest confidence)
- Removed dead code: Redis import from api.js, updateCardField from card-identity.js
- Set browser: 238 sets with logos, era groups, rarity filters, collection tracking
- Price trend signals: buy/wait/fair with 7d/30d changes, per-source breakdown
- Card-centric view with raw/graded tabs, grading ROI comparison
- Autocomplete: 29K cards (EN+JP) from TCGdex, cached in Firestore
- Security pipeline: Sigstore signing, Binary Authorization, SBOM/Grype, CodeQL SAST
- Multi-region deployment: asia-south1 + us-central1 with global HTTPS LB
- Custom Wolfi base image (0 CVEs), Terraform CI/CD
- Portfolio: value tracking, history, gainers/losers, CSV export, grading opportunities
- Email alerts via Resend (price drop + arbitrage), Cloud Scheduler
- Search: 200ms cached, relevance filtering, seller feedback, condition detection

## 1.0.0-beta.1 (2026-05-10)

Initial public beta.

### Added
- Consumer dashboard at /dashboard: search, arbitrage, price history, grade breakdown
- Admin dashboard at /admin: stats KPIs, developer key CRUD, error log viewer
- Cross-source arbitrage: /api/arbitrage compares prices across eBay, magi, Yahoo, SNKRDUNK
- Condition detection: auto-detects NM/LP/MP from EN + JP markers (状態A/美品)
- Condition filter: ?condition=nm works across all sources
- Price outlier flagging: listings >40% below median flagged
- Card identity: /api/card with canonical IDs, set resolution from card numbers
- Price history: /api/price-history tracks sold comp prices over time
- TCGPlayer integration: seeds price history when no data exists
- Scheduled price tracking: /api/track-prices for Cloud Scheduler
- Developer API key management: create, rotate, revoke, delete via Firestore
- Detail panel tabs: Grade / Prices to reduce scrolling
- Multi-source slab search: compare PSA 10 prices across eBay, magi.camp, Yahoo Auctions
- Per-subgrade AI grading: centering, corners, edges, surface graded independently in parallel
- Front + back image analysis with subgradeDetails (score, confidence, detail per attribute)
- PSA tier recommendations (Value/Regular/Express) with reasoning per card value
- REST API with CC_LIVE_ key auth + CC_LIVE_SANDBOX_ public sandbox key
- Rate limiting: 60/min auth, 20/min sample data, 5/min sandbox
- Firestore caching with stale-while-revalidate, per-key cache isolation
- Magi search migrated from Playwright to fetch+cheerio (~10x faster)
- eBay sold scrape retry with backoff on 503
- OAuth token pre-fetched on server startup
- Security: helmet headers, error sanitization, request IDs, trust proxy
- 143 tests (81 unit + 62 API integration)
- GitHub Actions CI on push/PR, auto-deploy on merge to main
- Chrome extension: queue auto-join for Pokemon Center, Walmart, Costco, Target
- Claude Code `/casecomp` skill for plain-English card search
- GitHub release v1.0.0-beta.1 with Chrome extension zip

### Infrastructure
- Cloud Run `casecomp-api` (API) + `casecomp-site` (frontend SSR with Cloud CDN)
- HTTPS LB routes by host: casecomp.xyz → site, api.casecomp.xyz → API
- Cloudflare SSL + edge caching for casecomp.xyz (~85ms TTFB, down from 1,210ms)
- GCP managed SSL for api.casecomp.xyz
- Firestore, Secret Manager (incl. sandbox key)
- Cloud Monitoring: error alerts + uptime check on /api/health
- Terraform with GCS state backend
- Workload Identity Federation for GitHub Actions → GCP (no stored keys)
- Kaniko layer caching for Cloud Build
- Branch protection on main: CI required before merge
- Multi-source search: eBay, magi.camp, Yahoo Auctions JP, SNKRDUNK
- AI pre-grading: per-subgrade (centering, corners, edges, surface) with PSA rubric
- Cross-source arbitrage detection
- PSA population data + submission tier recommendations
- REST API with key auth, rate limiting, Firestore caching
- Consumer dashboard + admin panel
- Chrome extension: drop queue auto-join
- Cloud Run + Terraform + GitHub Actions CI/CD
Loading
Loading