steam-backlog-enforcer/web/src/protondb.ts
Krzysztof kuhy Rudnicki 41deb90324 feat: add interactive web UI for backlog completion planning
Adds a React/TypeScript frontend (web/) with a Python stdlib HTTP server
backend.  The UI mirrors the CLI `stats` command in the browser, with
real-time sliders for ProtonDB rating, HLTB confidence thresholds, daily
play time, per-game time cap, playtime mode, no-HLTB-data fallback, and a
target-date planner.  A parity badge confirms the client-side totals
reproduce the CLI defaults exactly (786 / 67031.1h / 143017.2h / 238447.9h).

Python side:
- `_web_dataset.py`: offline projection of HLTB/ProtonDB/snapshot caches
  into a compact, secret-free JSON payload; 100% branch coverage
- `_web_server.py`: zero-dependency stdlib HTTP server serving the built
  Vite bundle and the /api/dataset endpoint; 100% branch coverage
- `main.py`: new `serve` command wiring

Frontend (Vitest + RTL, 100% branch coverage enforced):
- TypeScript port of ProtonDB compound rating rule with full parity
- Pure client-side filtering via estimate.ts (no server round-trips)
- SVG completion timeline chart, sortable/searchable game table
- Steam dark theme; responsive layout

Pre-commit: adds `vitest-coverage` hook at pre-push stage requiring 100%
branch coverage on the React codebase.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 18:35:45 +02:00

73 lines
2.1 KiB
TypeScript

// ProtonDB tier logic. `isPlayable` is a faithful port of
// ProtonDBRating.is_playable in steam_backlog_enforcer/protondb.py, so the
// UI's default ProtonDB filter reproduces the CLI's qualifying set exactly.
export const TIER_ORDER: Record<string, number> = {
native: 0,
platinum: 1,
gold: 2,
silver: 3,
bronze: 4,
borked: 5,
pending: 6,
}
// Tiers offered in the "minimum tier" dropdown, best → worst.
export const SELECTABLE_TIERS = [
'native',
'platinum',
'gold',
'silver',
'bronze',
] as const
const MIN_PLAYABLE_TIER = 'gold'
const UNKNOWN_RANK = 99
function rank(tier: string): number {
return TIER_ORDER[tier] ?? UNKNOWN_RANK
}
/**
* Faithful port of the CLI's compound playability rule.
*
* A game with no rating (or "pending") is not blocked. With a single rating,
* it must be gold-or-better. With both `tier` and `trending`, neither may be
* below silver and at least one must be gold-or-better.
*/
export function isPlayable(tier: string, trending: string): boolean {
if (!tier || tier === 'pending') return true
const tierRank = rank(tier)
const minRank = TIER_ORDER[MIN_PLAYABLE_TIER]
const silverRank = TIER_ORDER.silver
if (!trending) return tierRank <= minRank
const trendRank = rank(trending)
if (tierRank > silverRank || trendRank > silverRank) return false
return !(tierRank > minRank && trendRank > minRank)
}
/**
* Simple "minimum acceptable tier" rule for the manual ProtonDB mode.
*
* Uses the better (lower-rank) of the two available ratings. Games with no
* rating fall back to `treatMissingAsPass`.
*/
export function passesMinTier(
tier: string,
trending: string,
minTier: string,
treatMissingAsPass: boolean,
): boolean {
const present = [tier, trending].filter((t) => t && t !== 'pending')
if (present.length === 0) return treatMissingAsPass
const bestRank = Math.min(...present.map(rank))
return bestRank <= rank(minTier)
}
/** A short, human-readable label for a game's ProtonDB rating. */
export function tierLabel(tier: string, trending: string): string {
if (!tier) return '—'
if (trending && trending !== tier) return `${tier} / ${trending}`
return tier
}