diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b1b1009..b904983 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ on: branches: [main] jobs: - test: + unit: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -15,3 +15,20 @@ jobs: node-version: 24 - run: npm install - run: node test/unit-test.js + + smoke: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: 24 + - run: npm install + - run: npx playwright install chromium --with-deps + - run: node test/smoke-test.js + + test: + runs-on: ubuntu-latest + needs: [unit, smoke] + steps: + - run: echo "All tests passed" diff --git a/README.md b/README.md index 0848656..aa5d26e 100644 --- a/README.md +++ b/README.md @@ -56,24 +56,37 @@ yarn api # dashboard on localhost:3000 api.js Express API + static dashboard (public/) index.js CLI entry point lib/ - ebay.js eBay Browse API, OAuth, ship-to filtering - grading.js AI pre-grading (Claude/OpenAI), validation, caching - psa.js PSA pop reports, cert lookup, grading signal - magi.js magi.camp scraper + Haiku JP translation - yahooauctions.js Yahoo Auctions JP scraper - snkrdunk.js SNKRDUNK JSON API - filters.js Language, relevance, slab detection, blocklist - listingQuery.js eBay search query builder - firestore.js Firestore: grade logs, drops, webhooks, cache - demo.js Sample data (3 cards with real listings) - api-keys.js Firestore-backed developer key management - price-history.js Sold comp price tracking over time + sources/ + ebay.js eBay Browse API, OAuth, ship-to filtering + magi.js magi.camp scraper (fetch + cheerio) + snkrdunk.js SNKRDUNK JSON API + yahooauctions.js Yahoo Auctions JP scraper + tcgplayer.js TCGPlayer price seeding + grading/ + grading.js AI pre-grading (per-subgrade, Claude/OpenAI) + psa.js PSA pop reports, cert lookup, grading signal + psaTiers.js PSA submission tier data + data/ + firestore.js Firestore: grade logs, drops, webhooks, cache + api-keys.js Developer key management + card-identity.js Card identity: canonical IDs, set resolution + price-history.js Sold comp price tracking + demo.js Sample data (3 multi-source cards) + cache.js File-based cache (legacy) + redis-cache.js Redis cache (optional) + search/ + filters.js Language, relevance, condition detection, outlier flagging + listingQuery.js eBay search query builder + ebayCategories.js eBay category IDs + output.js Markdown/JSON formatters (CLI) swagger.js OpenAPI 3.0.3 spec +public/ Root dashboard (search, grade, arbitrage, price history) +public/admin/ Admin dashboard (keys, stats, errors) extension/ Chrome extension: queue auto-join, drop intel terraform/ GCP infra: Cloud Run ×2, Firestore, LB + CDN, Secret Manager test/ - unit-test.js 63 unit tests (filters, grading, query, demo data) - api-test.js 42 API integration tests + unit-test.js 81 unit tests + api-test.js 62 API integration tests ``` ## Web Dashboard diff --git a/api.js b/api.js index 6bfaf40..9eaccc1 100644 --- a/api.js +++ b/api.js @@ -17,7 +17,7 @@ import { buildEbaySearchQuery } from "./lib/search/listingQuery.js"; import { EBAY_CATEGORY_TCG_SINGLE_CARDS_US } from "./lib/search/ebayCategories.js"; import { getRedisStatus, sha256 } from "./lib/data/redis-cache.js"; import { saveGradeLog, getGradeLogs, saveDrop, getDrops, getDrop, saveWebhook, getWebhooks, deleteWebhook, getFirestoreStatus, saveAlert, saveErrorLog, getErrorLogs, clearErrorLogs } from "./lib/data/firestore.js"; -import { getDemoSearchResult, listDemoCards } from "./lib/demo.js"; +import { getDemoSearchResult, listDemoCards } from "./lib/data/demo.js"; import { createApiKey, listApiKeys, getApiKey, updateApiKey, deleteApiKey, rotateApiKey, validateApiKey } from "./lib/data/api-keys.js"; import { recordSoldPrices, getPriceHistory } from "./lib/data/price-history.js"; import { seedFromTCGPlayer } from "./lib/sources/tcgplayer.js"; diff --git a/index.js b/index.js index eec4c66..39f6bc4 100644 --- a/index.js +++ b/index.js @@ -22,14 +22,14 @@ import { testGradingProvider, printSiteGradingHelp, } from "./lib/grading/grading.js"; -import { writeMarkdown, writeJson, writePerCardJson, appendCombinedMarkdown, printSummary, mergeAndWrite } from "./lib/output.js"; +import { writeMarkdown, writeJson, writePerCardJson, appendCombinedMarkdown, printSummary, mergeAndWrite } from "./lib/search/output.js"; import { buildEbaySearchQuery, describeListingSearch } from "./lib/search/listingQuery.js"; import { EBAY_CATEGORY_TCG_SINGLE_CARDS_US } from "./lib/search/ebayCategories.js"; import { searchMagi } from "./lib/sources/magi.js"; import { searchYahooAuctions } from "./lib/sources/yahooauctions.js"; import { searchSnkrdunk } from "./lib/sources/snkrdunk.js"; import { getPsaGradingSignal } from "./lib/grading/psa.js"; -import { getDemoSearchResult, listDemoCards } from "./lib/demo.js"; +import { getDemoSearchResult, listDemoCards } from "./lib/data/demo.js"; export const CARDS = [ "Giratina V Alt Art" @@ -681,5 +681,5 @@ export { getCachedGrade, cacheGrade, } from "./lib/grading/grading.js"; -export { writeMarkdown, writeJson, writePerCardJson, appendCombinedMarkdown, printSummary } from "./lib/output.js"; +export { writeMarkdown, writeJson, writePerCardJson, appendCombinedMarkdown, printSummary } from "./lib/search/output.js"; export { buildEbaySearchQuery, describeListingSearch } from "./lib/search/listingQuery.js"; diff --git a/lib/demo.js b/lib/data/demo.js similarity index 100% rename from lib/demo.js rename to lib/data/demo.js diff --git a/lib/output.js b/lib/search/output.js similarity index 99% rename from lib/output.js rename to lib/search/output.js index c7d08c2..e15453b 100644 --- a/lib/output.js +++ b/lib/search/output.js @@ -1,9 +1,9 @@ import fs from "fs/promises"; import path from "path"; import { fileURLToPath } from "url"; -import { GRADING_COMPANY_TIERS, PSA_TIERS } from "./grading/psaTiers.js"; +import { GRADING_COMPANY_TIERS, PSA_TIERS } from "../grading/psaTiers.js"; -const __dirname = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const __dirname = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const OUTPUT_DIR = path.join(__dirname, "output"); // results.json / results.md stay in root; everything else goes in output/ diff --git a/package.json b/package.json index 320cfc3..495ad08 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "test:unit": "node test/unit-test.js", "test:api": "node test/api-test.js", "test:live": "API_URL=https://api.casecomp.xyz node test/api-test.js", + "test:smoke": "node test/smoke-test.js", "scan": "node scan.js", "psa": "node scripts/psa-report.js", "deploy": "gcloud builds submit --config=cloudbuild.yml --project casecomp-495718 && gcloud run deploy casecomp-api --image gcr.io/casecomp-495718/casecomp-api --region asia-south1 --project casecomp-495718 --port 3000 --allow-unauthenticated", diff --git a/public/app.js b/public/app.js index c4a9975..fa08846 100644 --- a/public/app.js +++ b/public/app.js @@ -21,6 +21,7 @@ let allItems = []; let allActive = []; let allSold = []; let activeSourceFilter = "all"; +let currentPsaSignal = null; document.querySelectorAll(".hint").forEach(h => { h.addEventListener("click", () => { @@ -110,6 +111,7 @@ function render(data) { ${noteHtml} `; + currentPsaSignal = data.psaSignal || null; renderPsa(data.psaSignal); allActive = active; @@ -317,13 +319,16 @@ function selectItem(itemId) { `; const fields = []; - if (item.condition) fields.push({ label: "Condition", value: item.condition }); + const condVal = item.condition + ? (slabLabel ? `${item.condition} Graded` : item.condition) + : (slabLabel ? `Graded` : ""); + if (condVal) fields.push({ label: "Condition", value: condVal, raw: true }); if (item.soldDate || item.endedDate) fields.push({ label: "Sold", value: item.soldDate || item.endedDate }); - if (item.priceJPY) fields.push({ label: "JPY", value: `¥${item.priceJPY.toLocaleString()}` }); + if (item.priceJPY) fields.push({ label: "Price (JPY)", value: `¥${item.priceJPY.toLocaleString()}` }); if (item.totalCost && item.totalCost !== item.price) fields.push({ label: "Item Price", value: formatPrice(item.price, item.priceCurrency) }); const fieldsHtml = fields.length ? `
${fields.map(f => ` -
${esc(f.label)}
${esc(f.value)}
+
${esc(f.label)}
${f.raw ? f.value : esc(f.value)}
`).join("")}
` : ""; const gradeHtml = renderGradeDetail(grade); @@ -356,6 +361,7 @@ function selectItem(itemId) { ${gradeHtml}
+ ${renderPsaInline(currentPsaSignal)}