mirror of
https://github.com/kuhyx/steam-backlog-enforcer.git
synced 2026-11-11 20:53:48 +01:00
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>
73 lines
2.1 KiB
TypeScript
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
|
|
}
|