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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Tests
on:
push:
branches: [main, dev]
branches: [main]
pull_request:
branches: [main]

Expand Down
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# Changelog

## Unreleased

### Added
- Playwright smoke test suite (40 tests): dashboard UI, detail panel, tabs, PSA stats, arbitrage, mobile viewport, static assets
- 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
- Arbitrage "Best Price" chip and savings summary
- Fade-up entrance animations, sticky frosted header, sticky search bar

### 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: added detectedCondition (NM/LP) based on AI grades
- Detail panel: prefer detectedCondition over "Ungraded"
- Consistent shipping display with green "Free shipping"
- CI: unit + smoke run in parallel, test gate job, removed duplicate dev push trigger
- Demo rate limit shown correctly as 360/min

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

Initial public beta.
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ terraform/ GCP infra: Cloud Run ×2, Firestore, LB + CDN, Secret Manage
test/
unit-test.js 81 unit tests
api-test.js 62 API integration tests
smoke-test.js 40 Playwright smoke tests (dashboard UI)
```

## Web Dashboard
Expand Down Expand Up @@ -139,7 +140,7 @@ curl -H "Authorization: Bearer $CASECOMP_KEY" \
| Endpoint | Limit |
|----------|-------|
| Authenticated (`CC_LIVE_` key) | 60 req/min |
| Sample data (`?demo=true`) | 20 req/min |
| Sample data (`?demo=true`) | 360 req/min |
| Health, docs, static | No limit |

### Public endpoints (no key)
Expand Down Expand Up @@ -226,11 +227,11 @@ Load unpacked from `extension/` in `chrome://extensions`.

## Tests

143 tests: 81 unit (filters, grading, query builder, card identity, condition detection, demo data) + 62 API (health, drops, webhooks, search, sold, PSA, grade, auth, admin keys, arbitrage, price-history, condition, demo validation).
183 tests: 81 unit (filters, grading, query builder, card identity, condition detection, demo data) + 62 API (health, drops, webhooks, search, sold, PSA, grade, auth, admin keys, arbitrage, price-history, condition, demo validation) + 40 Playwright smoke (dashboard UI, detail panel, tabs, PSA stats, arbitrage, mobile viewport).

## Contributing

Contributions welcome. Fork the repo, create a branch, and open a PR against `dev`. Run `yarn test` before submitting — all 105 tests must pass.
Contributions welcome. Fork the repo, create a branch, and open a PR against `dev`. Run `yarn test` before submitting — all tests must pass.

For bug reports or feature requests, open an [issue](https://github.com/Pyronewbic/casecomp/issues).

Expand Down
60 changes: 59 additions & 1 deletion public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,8 @@ function selectItem(itemId) {
${gradeHtml}
</div>
<div class="detail-tab-panel${defaultTab === "prices" ? "" : " hidden"}" data-dtpanel="prices">
${renderPsaInline(currentPsaSignal)}
${slabLabel ? renderPsaInline(currentPsaSignal) : ""}
<div id="grading-roi" class="grading-roi hidden"></div>
<div id="arbitrage-container" class="arbitrage-container hidden"></div>
<div id="price-chart-container" class="price-chart-container hidden">
<div class="detail-grade-section-label">Price History</div>
Expand Down Expand Up @@ -444,6 +445,7 @@ function selectItem(itemId) {
loadCardIdentity(currentQuery);
loadPriceChart(currentQuery);
loadArbitrage(currentQuery);
loadGradingRoi(item);
}

async function loadCardIdentity(query) {
Expand Down Expand Up @@ -515,6 +517,62 @@ async function loadArbitrage(query) {
} catch {}
}

function loadGradingRoi(item) {
const container = document.getElementById("grading-roi");
if (!container) return;
if (!currentPsaSignal || item.listingGradeLabel) return;

const psa = currentPsaSignal;
const rawPrice = item.totalCost || item.price;
if (!rawPrice || !psa.estCost) return;

const gradingCost = parseFloat(psa.estCost.replace(/[^0-9.]/g, ""));
if (!gradingCost) return;

const gemPct = psa.gem10Pct || 0;
const totalCost = rawPrice + gradingCost;
const diffClass = psa.difficulty === "easy" ? "easy" : psa.difficulty === "hard" || psa.difficulty === "brutal" ? "hard" : "moderate";

container.classList.remove("hidden");
const profitable = gemPct >= 50;
const verdictClass = profitable ? "roi-yes" : "roi-no";
const verdict = profitable ? "Worth grading" : "Risky";

container.innerHTML = `
<div class="detail-grade-section-label">Grade This Card?</div>
<div class="roi-grid">
<div class="roi-stat">
<div class="roi-label">Raw Price</div>
<div class="roi-value">${formatPrice(rawPrice, "USD")}</div>
</div>
<div class="roi-stat">
<div class="roi-label">${esc(psa.tier)} Grading</div>
<div class="roi-value">${esc(psa.estCost)}</div>
</div>
<div class="roi-stat">
<div class="roi-label">Total Cost</div>
<div class="roi-value roi-total">${formatPrice(totalCost, "USD")}</div>
</div>
<div class="roi-stat">
<div class="roi-label">Gem Rate</div>
<div class="roi-value">${gemPct}%</div>
</div>
</div>
<div class="roi-psa-row">
<span>Pop <b>${psa.totalPop ? psa.totalPop.toLocaleString() : "—"}</b></span>
<span>Difficulty <b class="${diffClass}">${esc(psa.difficulty || "—")}</b></span>
<span>PSA 10 <b>${psa.pop10 ? psa.pop10.toLocaleString() : "—"}</b></span>
<span>PSA 9 <b>${psa.pop9 ? psa.pop9.toLocaleString() : "—"}</b></span>
</div>
<div class="roi-verdict ${verdictClass}">
<span class="roi-verdict-label">${verdict}</span>
<span class="roi-verdict-detail">${gemPct >= 50
? `${gemPct}% gem rate at ${esc(psa.estCost)} grading — favorable odds`
: `${gemPct}% gem rate — high risk of PSA 9 or lower`}</span>
</div>
`;
}

async function loadPriceChart(query) {
const container = document.getElementById("price-chart-container");
const canvas = document.getElementById("price-chart");
Expand Down
1 change: 1 addition & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<span class="beta-badge">Beta</span>
</div>
<nav>
<a href="https://casecomp.xyz/developers" class="nav-link">Developers</a>
<a href="/docs" class="nav-link">API Docs</a>
</nav>
</header>
Expand Down
74 changes: 74 additions & 0 deletions public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ header {
line-height: 1;
}

nav { display: flex; gap: 20px; }

.nav-link {
color: var(--muted);
font-size: 14px;
Expand Down Expand Up @@ -698,6 +700,78 @@ main {
.card-id-sep { width: 1px; height: 14px; background: var(--border); flex-shrink: 0; }
.card-id-lang { font-size: 9px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.04em; margin-right: 2px; }

.grading-roi {
background: var(--inset);
border: 1px solid var(--border);
border-radius: 8px;
padding: 14px 16px;
margin-bottom: 14px;
}
.roi-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
margin-top: 8px;
}
.roi-stat { text-align: center; }
.roi-label {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
margin-bottom: 4px;
}
.roi-value {
font-family: 'Space Grotesk', system-ui, sans-serif;
font-size: 16px;
font-weight: 700;
color: var(--text);
}
.roi-total { color: var(--gold); }
.roi-psa-row {
display: flex;
gap: 16px;
justify-content: center;
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--border);
font-size: 11px;
color: var(--muted);
}
.roi-psa-row b {
color: var(--text);
font-family: 'Space Grotesk', system-ui, sans-serif;
margin-left: 3px;
}
.roi-psa-row b.easy { color: var(--green); }
.roi-psa-row b.hard { color: var(--red); }
.roi-psa-row b.moderate { color: var(--gold); }
.roi-verdict {
margin-top: 10px;
padding-top: 10px;
border-top: 1px solid var(--border);
display: flex;
align-items: center;
gap: 10px;
}
.roi-verdict-label {
font-family: 'JetBrains Mono', monospace;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 3px 8px;
border-radius: 4px;
flex-shrink: 0;
}
.roi-yes .roi-verdict-label { color: var(--green); background: rgba(124, 224, 168, 0.1); border: 1px solid rgba(124, 224, 168, 0.25); }
.roi-no .roi-verdict-label { color: var(--red); background: rgba(255, 93, 93, 0.08); border: 1px solid rgba(255, 93, 93, 0.25); }
.roi-verdict-detail {
font-size: 12px;
color: var(--muted);
}

.arbitrage-container { background: var(--inset); border: 1px solid var(--border); border-radius: 8px; padding: 14px 16px; margin-bottom: 14px; }
.arbitrage-sources { display: flex; gap: 8px; margin-top: 8px; }
.arb-source { flex: 1; background: var(--panel); border: 1px solid var(--border); border-radius: 6px; padding: 10px; text-align: center; }
Expand Down