Compare commits

...

2 Commits

Author SHA1 Message Date
josh 108b77ed39 feat: inline power play indicator as compact badge in card header
CI / Lint (push) Successful in 45s
CI / Test (push) Successful in 8s
CI / Build & Push (push) Successful in 1m8s
Moves the PP team + countdown into the badges row next to period and
clock, freeing up a full line of vertical space on each live card.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 11:05:38 -04:00
josh 61202b2a70 feat: sort scheduled games by start time instead of hype
Pre-game listings are more intuitive in chronological order than ranked
by pregame importance. LIVE and FINAL keep sorting by Priority desc.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 11:05:35 -04:00
4 changed files with 134 additions and 46 deletions
+9 -2
View File
@@ -76,6 +76,7 @@ def parse_games(scoreboard_data):
"total": total_priority, "total": total_priority,
}, },
"Start Time": get_start_time(game), "Start Time": get_start_time(game),
"Start Time UTC": game.get("startTimeUTC", ""),
"Home Record": format_record(game["homeTeam"]["record"]) "Home Record": format_record(game["homeTeam"]["record"])
if game["gameState"] in ["PRE", "FUT"] if game["gameState"] in ["PRE", "FUT"]
else "N/A", else "N/A",
@@ -98,8 +99,14 @@ def parse_games(scoreboard_data):
} }
) )
# Sort games based on priority def _sort_key(g):
return sorted(extracted_info, key=lambda x: x["Priority"], reverse=True) if g["Game State"] == "PRE":
# Earliest start first — ISO-8601 sorts correctly as a string
return (0, g["Start Time UTC"], 0)
# LIVE / FINAL — highest priority first
return (1, "", -g["Priority"])
return sorted(extracted_info, key=_sort_key)
def get_comeback_bonus(game): def get_comeback_bonus(game):
+3 -8
View File
@@ -69,12 +69,12 @@ function renderLiveGame(game) {
<div class="badges"> <div class="badges">
${periodLabel} ${periodLabel}
<span class="badge" ${clockAttrs}>${time}</span> <span class="badge" ${clockAttrs}>${time}</span>
${ppBadge(game)}
</div> </div>
${dot} ${dot}
</div> </div>
${teamRow(game, 'Away', 'live')} ${teamRow(game, 'Away', 'live')}
${teamRow(game, 'Home', 'live')} ${teamRow(game, 'Home', 'live')}
${ppIndicator(game)}
${hype} ${hype}
</div>`; </div>`;
} }
@@ -134,7 +134,7 @@ function teamRow(game, side, state) {
</div>`; </div>`;
} }
function ppIndicator(game) { function ppBadge(game) {
const awayPP = game['Away Power Play']; const awayPP = game['Away Power Play'];
const homePP = game['Home Power Play']; const homePP = game['Home Power Play'];
const pp = awayPP || homePP; const pp = awayPP || homePP;
@@ -145,12 +145,7 @@ function ppIndicator(game) {
const seconds = timeToSeconds(timeStr); const seconds = timeToSeconds(timeStr);
const attrs = `data-seconds="${seconds}" data-received-at="${Date.now()}" data-pp-clock`; const attrs = `data-seconds="${seconds}" data-received-at="${Date.now()}" data-pp-clock`;
return ` return `<span class="badge badge-pp">PP ${team} <span ${attrs}>${timeStr}</span></span>`;
<div class="pp-indicator">
<span class="pp-label">PP</span>
<span class="pp-team">${team}</span>
<span class="pp-clock" ${attrs}>${timeStr}</span>
</div>`;
} }
// ── Gauge ──────────────────────────────────────────── // ── Gauge ────────────────────────────────────────────
+4 -36
View File
@@ -212,44 +212,12 @@ main {
white-space: nowrap; white-space: nowrap;
} }
/* ── Power Play Indicator ───────────────────────── */ /* ── Power Play Badge (inline in card header) ─── */
.pp-indicator { .badge-pp {
display: flex; background: rgba(239, 68, 68, 0.15);
align-items: center;
gap: 0.4rem;
margin-top: 0.5rem;
padding: 0.35rem 0.5rem;
background: rgba(239, 68, 68, 0.1);
border: 1px solid rgba(239, 68, 68, 0.3);
border-radius: 6px;
}
.pp-label {
font-size: 0.6rem;
font-weight: 700;
letter-spacing: 0.08em;
color: var(--red); color: var(--red);
text-transform: uppercase; border: 1px solid rgba(239, 68, 68, 0.35);
flex-shrink: 0;
}
.pp-team {
font-size: 0.72rem;
font-weight: 600;
color: var(--text);
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pp-clock {
font-size: 0.72rem;
font-weight: 700;
color: var(--red);
flex-shrink: 0;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
+118
View File
@@ -123,6 +123,124 @@ class TestParseGames:
def test_returns_empty_list_for_empty_dict(self): def test_returns_empty_list_for_empty_dict(self):
assert parse_games({}) == [] assert parse_games({}) == []
def test_pre_games_sorted_by_start_time_ascending(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value={
"league_sequence": 16,
"league_l10_sequence": 16,
"division_abbrev": "ATL",
"conference_abbrev": "E",
"games_played": 40,
"wildcard_sequence": 16,
},
)
late = make_game(
game_state="FUT",
home_name="Rangers",
away_name="Devils",
start_time_utc="2024-04-10T22:00:00Z",
)
early = make_game(
game_state="FUT",
home_name="Bruins",
away_name="Canadiens",
start_time_utc="2024-04-10T19:00:00Z",
)
next_day = make_game(
game_state="FUT",
home_name="Kings",
away_name="Ducks",
start_time_utc="2024-04-11T00:30:00Z",
)
result = parse_games({"games": [late, next_day, early]})
pre_games = [g for g in result if g["Game State"] == "PRE"]
names = [g["Home Team"] for g in pre_games]
assert names == ["Bruins", "Rangers", "Kings"]
def test_live_games_still_sorted_by_priority_descending(self, mocker):
mocker.patch(
"app.games.get_team_standings",
return_value={
"league_sequence": 16,
"league_l10_sequence": 16,
"division_abbrev": "ATL",
"conference_abbrev": "E",
"games_played": 40,
"wildcard_sequence": 16,
},
)
# Late P3 tied — high priority
high = make_game(
game_state="LIVE",
home_name="Rangers",
away_name="Devils",
home_score=2,
away_score=2,
period=3,
seconds_remaining=60,
)
# Early P1 blowout — low priority
low = make_game(
game_state="LIVE",
home_name="Bruins",
away_name="Canadiens",
home_score=5,
away_score=0,
period=1,
seconds_remaining=900,
)
result = parse_games({"games": [low, high]})
live_games = [g for g in result if g["Game State"] == "LIVE"]
assert live_games[0]["Home Team"] == "Rangers"
assert live_games[1]["Home Team"] == "Bruins"
def test_pre_games_ignore_priority_even_if_nonzero(self, mocker):
# Give one team standings that maximize importance, another that minimize it
def fake_standings(name):
if name == "Bruins":
return {
"league_sequence": 1,
"league_l10_sequence": 1,
"division_abbrev": "ATL",
"conference_abbrev": "E",
"games_played": 80,
"wildcard_sequence": 18,
}
return {
"league_sequence": 32,
"league_l10_sequence": 32,
"division_abbrev": "PAC",
"conference_abbrev": "W",
"games_played": 10,
"wildcard_sequence": 30,
}
mocker.patch("app.games.get_team_standings", side_effect=fake_standings)
# Bruins game starts later but will have higher importance
high_hype_late = make_game(
game_state="FUT",
home_name="Bruins",
away_name="Maple Leafs",
start_time_utc="2024-04-10T23:00:00Z",
)
low_hype_early = make_game(
game_state="FUT",
home_name="Kings",
away_name="Ducks",
start_time_utc="2024-04-10T19:00:00Z",
)
result = parse_games({"games": [high_hype_late, low_hype_early]})
pre_games = [g for g in result if g["Game State"] == "PRE"]
# Lower-hype-but-earlier game must still appear first
assert pre_games[0]["Home Team"] == "Kings"
assert pre_games[1]["Home Team"] == "Bruins"
assert pre_games[1]["Priority"] > pre_games[0]["Priority"]
class TestGetPowerPlayInfo: class TestGetPowerPlayInfo:
def test_returns_empty_when_no_situation(self): def test_returns_empty_when_no_situation(self):