Files
LudosData/docker-compose.yml
T
ckochandClaude Opus 5 ca70bcef34 Add market value: tiered prices and an eBay Browse provider
Researched the options first. PriceCharting is the standard for retro prices
but requires a paid subscription for both its API and its bulk download.
eBay's sold-price data sits behind the Marketplace Insights API, which is a
limited release closed to new developers. The free game-price APIs cover
current digital storefronts, not physical retro copies. So there is no free
route to sold prices, and this uses eBay Browse — active listings, which are
asking prices, labelled as such rather than presented as valuations.

Schema now holds three prices per game (loose, CIB, new), with marketValue
as whichever tier matches that copy's condition. Changing a condition
re-prices from the stored tiers with no further lookup, and the dashboard
can later show both actual value and what a collection would be worth
complete.

The judgement lives in classification and aggregation, both pure and both
tested without credentials:

  * listings are sorted into tiers from their titles, and accessories,
    reproductions and multi-game lots are discarded — a "box only" listing
    at $45 counted as a copy would halve the loose estimate for a $130 cart
  * the discard qualifier is required. The first version matched a bare
    "box", which threw out "complete in box" and "with box and manual",
    i.e. most of the CIB tier, while keeping exactly the listings the
    filter existed to remove. A test asserting on tiers rather than counts
    caught it.
  * median with an interquartile trim, since one optimist asking 50x moves
    a mean and not a median
  * sample counts travel with the estimate, because a tier drawn from two
    listings warrants less confidence than one drawn from thirty

Credentials are optional: with none set, /api/prices/status reports
configured=false and refresh answers 503 with instructions, while the rest
of the app is unaffected.

101 backend tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 15:21:32 -04:00

76 lines
2.5 KiB
YAML

# LudosData — full stack.
#
# cp .env.example .env # then edit the secrets
# docker compose up --build
#
# The SPA is served by nginx on http://localhost:8080, which also reverse-proxies
# /api and /uploads to the API container. Because everything is same-origin in
# this setup, the browser never issues a cross-origin request and CORS is not in
# play at all — the API's CORS policy only matters for `ng serve` on :4200.
services:
api:
build:
context: ./backend
image: ludosdata-api
restart: unless-stopped
environment:
ASPNETCORE_ENVIRONMENT: Production
# Required. Startup fails loudly if this is missing or under 32 chars.
Jwt__Key: ${JWT_KEY:?JWT_KEY is required — see .env.example}
Jwt__Issuer: ${JWT_ISSUER:-LudosData}
Jwt__Audience: ${JWT_AUDIENCE:-LudosData}
Jwt__LifetimeMinutes: ${JWT_LIFETIME_MINUTES:-720}
# Creates the first account and imports the 105 games from the 2018 dump,
# but only while the database has no users at all.
Seed__Enabled: ${SEED_ENABLED:-true}
Seed__UserName: ${SEED_USERNAME:-}
Seed__Email: ${SEED_EMAIL:-}
Seed__Password: ${SEED_PASSWORD:-}
# Optional market-value lookups. Blank means the pricing endpoints report
# 503 and everything else carries on.
Ebay__ClientId: ${EBAY_CLIENT_ID:-}
Ebay__ClientSecret: ${EBAY_CLIENT_SECRET:-}
Ebay__UseSandbox: ${EBAY_USE_SANDBOX:-false}
# Only consulted when the SPA is served from somewhere other than nginx.
Cors__AllowedOrigins__0: ${CORS_ORIGIN:-http://localhost:8080}
Cors__AllowedOrigins__1: http://localhost:4200
volumes:
# SQLite file and uploaded box art. This is the only stateful thing in the
# stack — back this volume up and you have backed up everything.
- ludos-data:/data
expose:
- "8080"
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/health || exit 1"]
interval: 15s
timeout: 3s
retries: 5
start_period: 20s
web:
build:
context: ./frontend
image: ludosdata-web
restart: unless-stopped
depends_on:
api:
condition: service_healthy
ports:
- "${WEB_PORT:-8080}:8080"
healthcheck:
# 127.0.0.1, not localhost: nginx listens on IPv4 only, and BusyBox wget
# resolves localhost to ::1 first and gets connection-refused.
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:8080/ || exit 1"]
interval: 15s
timeout: 3s
retries: 5
start_period: 10s
volumes:
ludos-data: